@bitkyc08/opencodex 2.59.0 → 2.60.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 (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -338,7 +338,7 @@ export async function prepareResponsesTransport(
338
338
  route.provider = current;
339
339
  }
340
340
  adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
341
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
341
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
342
342
  );
343
343
  invalidateSameTargetRequest();
344
344
  return adapter;
@@ -619,7 +619,7 @@ export async function prepareResponsesTransport(
619
619
  ? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
620
620
  : undefined,
621
621
  );
622
- let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
622
+ let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
623
623
  const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
624
624
  && !isCanonicalOpenAiForwardProvider(adapterProvider)
625
625
  && ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
@@ -79,7 +79,11 @@ export async function executeResponsesRunTurn(
79
79
  >,
80
80
  sendBudgetState: Pick<
81
81
  ResponsesSendBudget,
82
- "adapterDispatchBudget" | "reserveCredentialHop" | "pendingHopPermit"
82
+ | "adapterDispatchBudget"
83
+ | "noteAdapterPhysicalSend"
84
+ | "noteAdapterRecoveryWithheld"
85
+ | "reserveCredentialHop"
86
+ | "pendingHopPermit"
83
87
  >,
84
88
  completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
85
89
  ): Promise<Response> {
@@ -100,7 +104,12 @@ export async function executeResponsesRunTurn(
100
104
  rememberKiroDeliveredFinalAnswer,
101
105
  responseStateOptions,
102
106
  } = requestState;
103
- const { adapterDispatchBudget, reserveCredentialHop } = sendBudgetState;
107
+ const {
108
+ adapterDispatchBudget,
109
+ noteAdapterPhysicalSend,
110
+ noteAdapterRecoveryWithheld,
111
+ reserveCredentialHop,
112
+ } = sendBudgetState;
104
113
  const { emptyCompletionGuardEnabled } = completionPolicy;
105
114
  const {
106
115
  cancelResponseCompletion,
@@ -146,7 +155,11 @@ export async function executeResponsesRunTurn(
146
155
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
147
156
  }
148
157
  await refreshRunTurnSelection();
149
- transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
158
+ // An adapter that reports its own sends accounts for the first one at the boundary that
159
+ // dispatches it. Logging here would claim a send that the adapter's own budget can still
160
+ // refuse, which is exactly what happens once earlier recovery has spent the allowance.
161
+ const reportsOwnSends = transportState.runTurnAdapter.reportsPhysicalSends === true;
162
+ if (!reportsOwnSends) transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
150
163
  const runTurnProviderFetch = providerFetch(
151
164
  route.provider,
152
165
  options.codexWsRuntimeIdentity,
@@ -169,6 +182,14 @@ export async function executeResponsesRunTurn(
169
182
  // The only way the request budget reaches a transport the adapter owns. Without it
170
183
  // a Cursor turn's inner ladder was three physical sends the cap read as one.
171
184
  ...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
185
+ onPhysicalSend: send => noteAdapterPhysicalSend(
186
+ logCtx.usageLogInputTokens,
187
+ // The attempt's own recovery kind still labels its first send when the adapter
188
+ // does not supply one of its own.
189
+ { ...send, ...(send.recovery ?? recovery ? { recovery: send.recovery ?? recovery } : {}) },
190
+ { includeFirst: reportsOwnSends },
191
+ ),
192
+ onRecoveryWithheld: noteAdapterRecoveryWithheld,
172
193
  },
173
194
  targetQueue.push,
174
195
  );
@@ -273,6 +294,7 @@ export async function executeResponsesRunTurn(
273
294
  route.modelId,
274
295
  route.provider,
275
296
  inboundWire,
297
+ route.staticPolicy,
276
298
  );
277
299
  const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
278
300
  if (!rotatedAdapter.runTurn) {
@@ -17,6 +17,7 @@ import {
17
17
  clampImageMaxRounds,
18
18
  } from "../../images";
19
19
  import type { ProviderAdapter } from "../../adapters/base";
20
+ import type { OcxParsedRequest } from "../../types";
20
21
  import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
21
22
  import {
22
23
  GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
@@ -155,6 +156,7 @@ export async function executeResponsesSidecars(
155
156
  const rotateSidecarProviderOn429 = async (
156
157
  retryAfter: string | null,
157
158
  responseHeaders?: Headers,
159
+ retryParsed?: OcxParsedRequest,
158
160
  ): Promise<ProviderAdapter | null> => {
159
161
  const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
160
162
  retryAfter,
@@ -196,7 +198,7 @@ export async function executeResponsesSidecars(
196
198
  try {
197
199
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
198
200
  transportState.genericFailovers += 1;
199
- if (!await applyFailoverSnapshot(snapshot)) {
201
+ if (!await applyFailoverSnapshot(snapshot, retryParsed)) {
200
202
  hop.permit?.release();
201
203
  return null;
202
204
  }
@@ -254,9 +256,22 @@ export async function executeResponsesSidecars(
254
256
  return null;
255
257
  }
256
258
  const rotatedAdapter = resolveSelectionAdapter(
257
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
259
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
258
260
  config.cacheRetention,
259
261
  );
262
+ // The sidecar loops build each attempt from an iteration-local shallow copy of parsed, so a
263
+ // rebind that lands only on the outer request never reaches the wire: the retry would pair
264
+ // the new bearer with the previous account's Kiro routing metadata and continuation identity.
265
+ // Rebind the exact request the rotated adapter will be built from (same pattern as the
266
+ // continuation loop's nextParsed); when the hook ran without one, the outer bind below stands.
267
+ if (retryParsed && retryParsed !== parsed) {
268
+ bindRouteReasoningReplayScope({
269
+ parsed: retryParsed,
270
+ providerName: route.providerName,
271
+ provider: route.provider,
272
+ adapterName: rotatedAdapter.name,
273
+ });
274
+ }
260
275
  bindRouteReasoningReplayScope({
261
276
  parsed,
262
277
  providerName: route.providerName,
@@ -29,33 +29,14 @@ export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_M
29
29
  export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
30
30
 
31
31
  /**
32
- * Dial URL for a request URL. The canonical ChatGPT backend keeps its constant;
33
- * an operator-opted OpenAI-compatible upstream swaps https for wss on the same
34
- * path so gateways that serve the Responses WebSocket protocol on their
35
- * /v1/responses path get the same fast lane. Plain HTTP remains on SSE because
36
- * a provider WS handshake would otherwise send credentials and request data
37
- * without transport encryption.
32
+ * Dial URL for a first-party Responses endpoint. The canonical ChatGPT backend
33
+ * keeps its constant; the api.openai.com Responses endpoint swaps https for wss
34
+ * on the same path. No other upstream may enter the WebSocket lane.
38
35
  */
39
36
  function wsUpstreamUrlFor(httpUrl: string): string {
40
37
  if (httpUrl === CODEX_RESPONSES_HTTP_URL) return CODEX_RESPONSES_WS_URL;
41
- return httpUrl.replace(/^http(s?):/, "ws$1:");
42
- }
43
-
44
- /**
45
- * An operator-opted OpenAI-compatible upstream only joins the WS lane for
46
- * Responses endpoints: the WebSocket path speaks the Responses event protocol,
47
- * and every downstream consumer (adapter parsers, usage sniffing, SSE relay)
48
- * assumes that wire. Other paths (chat completions, images, search) stay HTTP.
49
- */
50
- function isResponsesWebsocketEligibleUrl(url: string): boolean {
51
- let parsed: URL;
52
- try {
53
- parsed = new URL(url);
54
- } catch {
55
- return false;
56
- }
57
- return parsed.protocol === "https:"
58
- && parsed.pathname.endsWith("/responses");
38
+ if (httpUrl === OPENAI_API_RESPONSES_URL) return httpUrl.replace(/^http(s?):/, "ws$1:");
39
+ throw new Error("unsupported Codex WebSocket upstream");
59
40
  }
60
41
  export type BunRuntimeIdentity = {
61
42
  version: string;
@@ -108,8 +89,13 @@ export function shouldUseCodexWsUpstream(
108
89
  ): boolean {
109
90
  if (!bunSupportsBoundedCodexWsRelay(runtime)) return false;
110
91
  if (socks5ProxyFromEnv()) return false;
111
- if (url !== CODEX_RESPONSES_HTTP_URL && !upstreamWebsocketConfigured) return false;
112
- if (upstreamWebsocketConfigured && !isResponsesWebsocketEligibleUrl(url)) return false;
92
+ // Bun's client WebSocket API delivers only fully assembled messages and has
93
+ // no enforceable inbound payload limit. Keep arbitrary provider endpoints on
94
+ // bounded HTTP/SSE until the client can reject fragmented text and binary
95
+ // messages during ingestion rather than after allocation. The first-party
96
+ // api.openai.com lane still requires the operator opt-in.
97
+ if (url !== CODEX_RESPONSES_HTTP_URL
98
+ && !(upstreamWebsocketConfigured && url === OPENAI_API_RESPONSES_URL)) return false;
113
99
  if ((init?.method ?? "GET").toUpperCase() !== "POST") return false;
114
100
  const body = init?.body;
115
101
  if (typeof body !== "string") return false;
@@ -139,7 +125,8 @@ export function codexWsUpstreamFetch(
139
125
  const prepared = prepareCodexWsRequest(url, init);
140
126
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
141
127
  init = prepared.httpInit;
142
- if (!bunSupportsBoundedCodexWsRelay(runtime)) {
128
+ if ((url !== CODEX_RESPONSES_HTTP_URL && url !== OPENAI_API_RESPONSES_URL)
129
+ || !bunSupportsBoundedCodexWsRelay(runtime)) {
143
130
  return sseFallback(url, init);
144
131
  }
145
132
  const signal = init.signal ?? undefined;
@@ -1,6 +1,7 @@
1
1
  import type { TranslatorBudget } from "../lib/translator-budget";
2
2
  import { mayBecomePatchEnvelope, normalizeApplyPatchDelimiters } from "../responses/apply-patch-envelope";
3
3
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
4
+ import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
4
5
  import { declaresCodeModeExec } from "../types/tools";
5
6
  import {
6
7
  customToolItemId,
@@ -15,50 +16,10 @@ import {
15
16
  type SseBlockRewrite,
16
17
  } from "./sse-payload-rewrite";
17
18
 
18
- /** Exact compact prefix used by our upstream rewriter; progressive matching also
19
- * tolerates insignificant JSON whitespace via FREEFORM_WRAP_PREFIX_RE. */
20
- const FREEFORM_WRAP_PREFIX = '{"input":"';
21
- const FREEFORM_WRAP_PREFIX_RE = /^\s*\{\s*"input"\s*:\s*"/;
22
-
23
19
  function isPlainObject(value: unknown): value is Record<string, unknown> {
24
20
  return !!value && typeof value === "object" && !Array.isArray(value);
25
21
  }
26
22
 
27
- /**
28
- * Progressive decode of the freeform `{ "input": "…" }` wrapper.
29
- * Returns null when the accumulated text does not (yet) match that wrapper so
30
- * callers suppress deltas and rely on `response.custom_tool_call_input.done`.
31
- */
32
- function partialCustomToolInput(argumentsText: string): string | null {
33
- const match = FREEFORM_WRAP_PREFIX_RE.exec(argumentsText);
34
- if (!match) return null;
35
- const body = argumentsText.slice(match[0]!.length);
36
- let output = "";
37
- for (let index = 0; index < body.length; index++) {
38
- const char = body[index];
39
- if (char === '"') break;
40
- if (char !== "\\") {
41
- output += char;
42
- continue;
43
- }
44
- const escaped = body[index + 1];
45
- if (escaped === undefined) break;
46
- index += 1;
47
- if (escaped === "n") output += "\n";
48
- else if (escaped === "t") output += "\t";
49
- else if (escaped === "r") output += "\r";
50
- else if (escaped === "b") output += "\b";
51
- else if (escaped === "f") output += "\f";
52
- else if (escaped === "u") {
53
- const hex = body.slice(index + 1, index + 5);
54
- if (hex.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) break;
55
- output += String.fromCharCode(Number.parseInt(hex, 16));
56
- index += 4;
57
- } else output += escaped;
58
- }
59
- return output;
60
- }
61
-
62
23
  function replaceSseEventName(block: string, type: string): string {
63
24
  const newline = block.includes("\r\n") ? "\r\n" : "\n";
64
25
  const lines = block.split(/\r?\n/);
@@ -332,21 +293,33 @@ export function createRoutedCustomToolRestoreBlockRewrite(
332
293
  openCalls.set(upstreamItemId, open);
333
294
  // A helper alias will become JavaScript at completion, never raw patch/JSON.
334
295
  if (itemNames.get(upstreamItemId)?.aliased) return [];
335
- // Still accumulating toward the compact wrapper, or an unrecognized shape:
336
- // suppress progressive emission and let the done event carry input.
337
- if (FREEFORM_WRAP_PREFIX.startsWith(open.argumentsText)) return [];
338
- const fullInput = partialCustomToolInput(open.argumentsText);
296
+ const itemName = itemNames.get(upstreamItemId);
297
+ const ownsFreeformGrammar = itemName?.namespace === undefined
298
+ || itemName?.namespace === "functions";
299
+ const fullInput = progressiveFreeformInput(
300
+ open.argumentsText,
301
+ ownsFreeformGrammar ? itemName?.name ?? "" : "",
302
+ );
339
303
  if (fullInput === null) return [];
340
- // Hold a buffer that could still become a complete patch envelope. The done event
341
- // recompiles such a body into an apply_patch helper call, so streaming the envelope
342
- // bytes first and replacing them at completion is the rewind this path forbids.
343
- // Mirrors the same hold in `src/bridge.ts`.
344
- if (
345
- declaresCodeModeExec(declaredNames)
346
- && itemNames.get(upstreamItemId)?.namespace === undefined
347
- && itemNames.get(upstreamItemId)?.name === "exec"
348
- && mayBecomePatchEnvelope(fullInput)
349
- ) return [];
304
+ // This routed path historically holds every unrecognized JSON object until done.
305
+ // The shared decoder streams ordinary raw input, so retain the stricter routed rule
306
+ // when no recognized wrapper transformed the accumulated object.
307
+ if (fullInput === open.argumentsText && open.argumentsText.trimStart().startsWith("{")) return [];
308
+ // Hold a buffer that could still become a complete patch envelope, for either of the two
309
+ // reasons completion rewrites one. Both are the same rewind this path forbids, and both
310
+ // mirror `src/bridge/sse.ts`.
311
+ //
312
+ // `exec`: the done event recompiles such a body into an apply_patch helper call.
313
+ // `apply_patch`: `normalizeApplyPatchDelimiters` rewrites a decorated
314
+ // `*** Begin Patch ***` envelope at completion, so the decorated markers would be
315
+ // published and then replaced by the normalized ones. Before the shared decoder, this
316
+ // path held every non-canonical shape and so never reached that case; now that ordinary
317
+ // raw input streams, the second reason has to be stated explicitly.
318
+ const mayCompile = declaresCodeModeExec(declaredNames)
319
+ && itemName?.namespace === undefined
320
+ && itemName?.name === "exec";
321
+ const mayNormalize = ownsFreeformGrammar && itemName?.name === "apply_patch";
322
+ if ((mayCompile || mayNormalize) && mayBecomePatchEnvelope(fullInput)) return [];
350
323
  if (!fullInput.startsWith(open.emittedInput) || fullInput.length === open.emittedInput.length) return [];
351
324
  const inputDelta = fullInput.slice(open.emittedInput.length);
352
325
  open.emittedInput = fullInput;
@@ -1,4 +1,9 @@
1
- import { collectAmbiguousDottedAliases, dottedAliasIsUnambiguous, wireToolInnerName } from "../responses/tool-name-aliases";
1
+ import {
2
+ collectAmbiguousDottedAliases,
3
+ dottedAliasIsUnambiguous,
4
+ isSchemaValidResponsesToolName,
5
+ wireToolInnerName,
6
+ } from "../responses/tool-name-aliases";
2
7
  import {
3
8
  dottedToolName,
4
9
  NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
@@ -512,6 +517,31 @@ export function normalizeDefaultNamespaceInItem(
512
517
  return { value: { ...item, name: bare }, changed: true };
513
518
  }
514
519
  }
520
+ // Authorization and emission were reading two different resolvers for the same question.
521
+ // `undeclaredNameInItem` resolves an emitted name through `normalizeDeclaredToolName`, which
522
+ // maps a `default.`-prefixed code-mode helper onto the declared `exec` (#4412) as well as a
523
+ // `default.`-prefixed bare tool (#4176); the rewrite above learned only the second case. So a
524
+ // routed Muse turn under a code-mode catalog had `default.view_image` ACCEPTED as `exec` and
525
+ // then relayed under the name the upstream schema rejects. Codex stored
526
+ // `{"type":"function_call","name":"default.view_image"}`, answered "unsupported call", and
527
+ // every later replay of that history — a side chat, a compaction — was refused on
528
+ // `input[N].name` for the lifetime of the conversation (#5095).
529
+ //
530
+ // Emit the name the guard authorized rather than a second, weaker opinion about it. The two
531
+ // must agree: a name good enough to admit is the name the client has to receive.
532
+ //
533
+ // Gated on the name actually being unusable, so this branch cannot touch a name the upstream
534
+ // accepts whatever the resolver would have said about it. The routed-custom-tool and
535
+ // namespace restores run earlier in the same chain and already rewrite the shapes they own,
536
+ // which leaves this as the boundary check for the names no restore claimed.
537
+ if (!isSchemaValidResponsesToolName(name)) {
538
+ const authorized = normalizeDeclaredToolName(name, declared, declaredBare);
539
+ if (
540
+ authorized !== name
541
+ && declared.has(authorized)
542
+ && isSchemaValidResponsesToolName(authorized)
543
+ ) return { value: { ...item, name: authorized }, changed: true };
544
+ }
515
545
  }
516
546
  return { value: item, changed: false };
517
547
  }
@@ -230,7 +230,7 @@ export function replaceSseDataPayload(block: string, payload: string): string {
230
230
  continue;
231
231
  }
232
232
  if (!replaced) {
233
- rewritten.push(`data: ${payload}`);
233
+ rewritten.push(...payload.split(/\r?\n/).map(line => `data: ${line}`));
234
234
  replaced = true;
235
235
  }
236
236
  }
@@ -116,6 +116,88 @@ function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
116
116
  }
117
117
  }
118
118
 
119
+ function Parse-StartupHealthText([string]$Text) {
120
+ foreach ($line in ($Text -split "\r?\n")) {
121
+ $trimmed = $line.Trim()
122
+ if (-not $trimmed) { continue }
123
+ try {
124
+ $parsed = $trimmed | ConvertFrom-Json
125
+ if ($parsed.status -in @("native", "protected", "at-risk") -and ($parsed.rebootSafe -is [bool])) {
126
+ return $parsed
127
+ }
128
+ } catch {
129
+ # A non-JSON line from the CLI (for example a diagnostic banner) is not a
130
+ # startup-health payload; keep scanning for the actual JSON result.
131
+ continue
132
+ }
133
+ }
134
+ return $null
135
+ }
136
+
137
+ function Start-StartupHealthProbe {
138
+ # Startup safety is machine-local diagnostic state, so the tray asks the CLI to
139
+ # collect it directly (ocx __startup-health) instead of poking the management API,
140
+ # which is admin-token gated. The CLI collects the diagnostic straight from the OS,
141
+ # so it works without the proxy answering and without any credential.
142
+ #
143
+ # The child writes into redirected pipes that the tick reads asynchronously: only
144
+ # the completed Task<string> is ever touched, so a slow or hung diagnostic can never
145
+ # block the Windows Forms UI thread, and its lifetime is bounded by a kill in the
146
+ # tick that lets the next refresh replace it instead of stacking another process.
147
+ try {
148
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
149
+ $psi.FileName = $BunPath
150
+ $psi.Arguments = ((@($CliPath, "__startup-health") | ForEach-Object { ConvertTo-NativeArgument $_ }) -join " ")
151
+ $psi.UseShellExecute = $false
152
+ $psi.CreateNoWindow = $true
153
+ $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
154
+ $psi.RedirectStandardOutput = $true
155
+ $psi.RedirectStandardError = $true
156
+ $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
157
+ $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
158
+ $psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome
159
+ $psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
160
+ if ($BunRuntimeSource) {
161
+ $psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
162
+ $psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
163
+ }
164
+ $process = [System.Diagnostics.Process]::Start($psi)
165
+ $script:startupProbeProcess = $process
166
+ # Drain both pipes asynchronously from the start so neither can fill and stall
167
+ # the child while the tick is only watching the output task's completion.
168
+ $script:startupProbeOutputTask = $process.StandardOutput.ReadToEndAsync()
169
+ $script:startupProbeErrorTask = $process.StandardError.ReadToEndAsync()
170
+ $script:startupProbeStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
171
+ } catch {
172
+ Write-ActionLog "startup-health probe launch failed: $($_.Exception.GetType().Name)"
173
+ if ($null -ne $script:startupProbeProcess) {
174
+ # Process.Start succeeded but the async pipe setup failed: Dispose alone
175
+ # would leave the Bun child running, so terminate it before cleanup.
176
+ try {
177
+ $script:startupProbeProcess.Kill()
178
+ [void]$script:startupProbeProcess.WaitForExit(3000)
179
+ } catch {
180
+ Write-ActionLog "startup-health probe launch cleanup failed: $($_.Exception.GetType().Name)"
181
+ }
182
+ }
183
+ Complete-StartupHealthProbe
184
+ }
185
+ }
186
+
187
+ function Complete-StartupHealthProbe {
188
+ $process = $script:startupProbeProcess
189
+ $script:startupProbeProcess = $null
190
+ $script:startupProbeOutputTask = $null
191
+ $script:startupProbeErrorTask = $null
192
+ if ($null -ne $process) {
193
+ try {
194
+ $process.Dispose()
195
+ } catch {
196
+ Write-ActionLog "startup-health probe dispose failed: $($_.Exception.GetType().Name)"
197
+ }
198
+ }
199
+ }
200
+
119
201
  function Read-ListenTarget {
120
202
  foreach ($path in @((Join-Path $OpenCodexHome "runtime-port.json"), (Join-Path $OpenCodexHome "config.json"))) {
121
203
  try {
@@ -172,6 +254,15 @@ $exitItem = $menu.Items.Add("Exit Tray")
172
254
  $script:online = $false
173
255
  $script:port = 10100
174
256
  $script:proxyPid = $null
257
+ $script:wasOnline = $false
258
+ $script:startupHealth = $null
259
+ $script:startupHealthCheckedAt = 0L
260
+ $script:startupRefreshMs = 20000L
261
+ $script:startupProbeProcess = $null
262
+ $script:startupProbeOutputTask = $null
263
+ $script:startupProbeErrorTask = $null
264
+ $script:startupProbeStarted = 0L
265
+ $script:startupProbeTimeoutMs = 30000L
175
266
  $script:pendingAction = $null
176
267
  $script:pendingStarted = 0L
177
268
  $script:pendingDeadline = 0L
@@ -228,18 +319,66 @@ function Update-TrayState {
228
319
  $pidMatches = $null -eq $target.pid -or [int]$target.pid -eq [int]$health.pid
229
320
  $script:online = $null -ne $health -and $health.status -eq "ok" -and $health.service -eq "opencodex" -and [int]$health.port -eq $script:port -and $pidMatches
230
321
  $script:proxyPid = if ($script:online) { [int]$health.pid } else { $null }
322
+ $cameOnline = $script:online -and -not $script:wasOnline
323
+ $script:wasOnline = $script:online
324
+ $now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
325
+ if ($null -ne $script:startupProbeProcess) {
326
+ $probeExited = $false
327
+ try {
328
+ $probeExited = $script:startupProbeProcess.HasExited
329
+ } catch {
330
+ $probeExited = $true
331
+ }
332
+ $probeTimedOut = ($now - $script:startupProbeStarted) -gt $script:startupProbeTimeoutMs
333
+ if ($probeExited -and $null -ne $script:startupProbeOutputTask -and $script:startupProbeOutputTask.IsCompleted) {
334
+ $probeStartup = $null
335
+ try {
336
+ $probeStartup = Parse-StartupHealthText $script:startupProbeOutputTask.Result
337
+ } catch {
338
+ Write-ActionLog "startup-health probe read failed: $($_.Exception.GetType().Name)"
339
+ }
340
+ $script:startupHealth = $probeStartup
341
+ Complete-StartupHealthProbe
342
+ $script:startupHealthCheckedAt = $now
343
+ } elseif ($probeTimedOut) {
344
+ # A hung diagnostic must not survive its refresh slot: terminate it so the
345
+ # next refresh cannot stack another orphaned Bun process behind it.
346
+ Write-ActionLog "startup-health probe timed out; terminating it"
347
+ try {
348
+ $script:startupProbeProcess.Kill()
349
+ [void]$script:startupProbeProcess.WaitForExit(3000)
350
+ } catch {
351
+ Write-ActionLog "startup-health probe kill failed: $($_.Exception.GetType().Name)"
352
+ }
353
+ $script:startupHealth = $null
354
+ Complete-StartupHealthProbe
355
+ $script:startupHealthCheckedAt = $now
356
+ }
357
+ }
358
+ $refreshDue =
359
+ $script:startupHealthCheckedAt -eq 0 -or
360
+ ($now - $script:startupHealthCheckedAt -gt $script:startupRefreshMs)
361
+ if ($script:online -and ($cameOnline -or $refreshDue) -and $null -eq $script:startupProbeProcess) {
362
+ # Record the attempt so launch failures and invalid results remain throttled.
363
+ $script:startupHealthCheckedAt = $now
364
+ Start-StartupHealthProbe
365
+ }
231
366
  if ($script:online) {
232
367
  $statusItem.Text = "Proxy: Online (port $($script:port))"
233
368
  $notify.Text = "opencodex: Online"
234
369
  $startItem.Enabled = $false
235
370
  $stopItem.Enabled = $true
236
371
  $restartItem.Enabled = $true
237
- try {
238
- $startup = Read-JsonUrl "$origin/api/startup-health"
372
+ # The management API is admin-token gated, so the tray must not poke
373
+ # /api/startup-health for the icon. The probe lifecycle maintenance and the
374
+ # refresh gate above run on every tick (even while offline) so a hung
375
+ # diagnostic is cleaned up outside the online-only UI branch.
376
+ $startup = $script:startupHealth
377
+ if ($null -ne $startup) {
239
378
  $label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" }
240
379
  $safetyItem.Text = "Restart safety: $label"
241
380
  $notify.Icon = if ($startup.status -eq "at-risk") { $warningIcon } else { $onlineIcon }
242
- } catch {
381
+ } else {
243
382
  $safetyItem.Text = "Restart safety: unavailable"
244
383
  $notify.Icon = $warningIcon
245
384
  }
@@ -360,6 +499,19 @@ try {
360
499
  if ($null -ne $script:pendingProcess) {
361
500
  try { $script:pendingProcess.Dispose() } catch { $null = $_ }
362
501
  }
502
+ if ($null -ne $script:startupProbeProcess) {
503
+ # A probe still running when the tray shuts down would outlive it: Dispose()
504
+ # releases the handle but does not terminate the child, so kill the active
505
+ # probe, wait briefly for exit, then release the probe state.
506
+ try {
507
+ Write-ActionLog "terminating active startup-health probe on tray shutdown"
508
+ $script:startupProbeProcess.Kill()
509
+ [void]$script:startupProbeProcess.WaitForExit(3000)
510
+ } catch {
511
+ Write-ActionLog "startup-health probe shutdown kill failed: $($_.Exception.GetType().Name)"
512
+ }
513
+ Complete-StartupHealthProbe
514
+ }
363
515
  $notify.Dispose()
364
516
  foreach ($icon in $script:ownedIcons) { $icon.Dispose() }
365
517
  $menu.Dispose()
@@ -403,6 +403,8 @@ export interface OcxConfig {
403
403
  client?: OcxClientConnectionConfig;
404
404
  /** Operator-facing redaction policy for management and CLI projections. */
405
405
  privacy?: OcxPrivacyConfig;
406
+ /** Opt-in process-local aggregate request metrics on the authenticated management plane. */
407
+ metricsExport?: { enabled?: boolean };
406
408
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
407
409
  emptyCompletionRetry?: boolean;
408
410
  /** Suppress allowlisted client-facing Codex transport hints; provider enforcement is unchanged. */
@@ -616,6 +618,12 @@ export interface OcxConfig {
616
618
  subagentEffortCap?: string;
617
619
  /** Global model effort overrides, after provider model/wide pins; none means omission. */
618
620
  modelPinnedEfforts?: Record<string, string>;
621
+ compactionRouting?: {
622
+ model: string;
623
+ reasoningEffort?: string;
624
+ /** Compaction triggers this override covers; omission means `["manual"]`. */
625
+ triggers?: ("manual" | "auto")[];
626
+ };
619
627
  /**
620
628
  * Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids
621
629
  * are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only
@@ -753,9 +761,9 @@ export interface OcxConfig {
753
761
  * into ALL_PROXY, clear inherited HTTP(S)_PROXY, and use OpenCodex's SOCKS5 transport.
754
762
  * Loopback stays in NO_PROXY.
755
763
  * The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`)
756
- * once at process start; on other platforms, or when the system proxy is off, SOCKS-only,
757
- * or unreadable, it degrades to direct egress with one log line (#1525). PAC/WPAD and live
758
- * changes are not followed.
764
+ * once at process start, preserving separate HTTP and HTTPS entries; on other platforms, or
765
+ * when the system proxy is off, SOCKS-only, or unreadable, it degrades to direct egress with
766
+ * one log line (#1525). PAC/WPAD and live changes are not followed.
759
767
  */
760
768
  proxy?: string;
761
769
  /**
@@ -356,6 +356,22 @@ export interface OcxProviderConfig {
356
356
  * `ocxr1` envelopes are still stripped because no upstream can decrypt them.
357
357
  */
358
358
  preserveResponsesReasoningContent?: boolean;
359
+ /**
360
+ * Treat this provider's `modelReasoningEfforts` as authoritative at the wire, not only in the
361
+ * catalog. Adapters that ship their own per-model effort table (currently `command-code`)
362
+ * otherwise let that table win for models it knows, so a widened row is advertised in the
363
+ * picker and then stripped on the way out. Opt-in because presets are SEEDED with the shipped
364
+ * table: without a declared flag there is no way to tell an operator's row from a copy an
365
+ * older release persisted. A rung the upstream then refuses is returned as that error rather
366
+ * than silently retried without the effort, since the operator asked for it.
367
+ */
368
+ modelReasoningEffortsAuthoritative?: boolean;
369
+ /**
370
+ * Drop replayed Responses `reasoning` items from input history before forwarding.
371
+ * Some OpenAI-compatible Responses upstreams accept tool-call replay but reject
372
+ * reasoning output items when they are sent back on a continuation.
373
+ */
374
+ dropResponsesReasoningItems?: boolean;
359
375
  /**
360
376
  * Explicit opt-in for a relay that genuinely fronts OpenAI and can decode native
361
377
  * compaction blobs. Absent or false degrades foreign blobs to an opaque note.
@@ -901,6 +917,8 @@ export interface OcxProviderConfig {
901
917
  * "cloud-code-assist" = Google Antigravity (Cloud Code Assist) OAuth + CCA envelope.
902
918
  */
903
919
  googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
920
+ /** Google tool-schema compatibility policy. Omitted preserves compatible report-only behavior. */
921
+ googleToolSchemaPolicy?: "compatible" | "reject-lossy";
904
922
  /** Vertex AI GCP project id (or GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env). */
905
923
  project?: string;
906
924
  /** Vertex AI location, e.g. "us-central1" or "global" (or GOOGLE_CLOUD_LOCATION env). */
@@ -126,6 +126,8 @@ export interface OcxParsedRequest {
126
126
  * (see src/responses/compaction.ts).
127
127
  */
128
128
  _compactionRequest?: boolean;
129
+ /** Manual compaction moved to another provider: summarize portably even on a canonical ChatGPT target. */
130
+ _portableCompaction?: boolean;
129
131
  /**
130
132
  * True when the current request newly introduced a stored compaction summary/marker. Historical
131
133
  * markers restored by previous_response_id expansion were already acknowledged and do not reset
@@ -67,6 +67,20 @@ const CODE_MODE_HELPER_TOOL_NAMES = [
67
67
  */
68
68
  export const CODE_MODE_EXEC_TOOL_NAME = "exec";
69
69
 
70
+ /**
71
+ * The nested-helper spellings, as a membership view of the same list.
72
+ *
73
+ * A code-mode catalog never DECLARES any of them — they exist only as `tools.<helper>(...)` inside
74
+ * `exec` — so a recorded call under one of these names can only have come from a provider echoing
75
+ * the helper, which is what makes the set usable as a bounded recovery vocabulary for stored
76
+ * history (#5095). Kept beside the tuple it is built from so the two can never drift; this is a
77
+ * different question from `NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES` below, which also covers `exec`
78
+ * itself because declaring THAT name is what turns normalization on.
79
+ */
80
+ export const CODE_MODE_HELPER_WIRE_NAMES: ReadonlySet<string> = new Set<string>(
81
+ CODE_MODE_HELPER_TOOL_NAMES,
82
+ );
83
+
70
84
  /**
71
85
  * Spellings that may never be MANUFACTURED as a bare alias for a namespaced tool.
72
86
  *
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  export type { OcxTool, OcxToolChoice } from "./types/tools";
5
5
  export {
6
6
  CODE_MODE_EXEC_TOOL_NAME,
7
+ CODE_MODE_HELPER_WIRE_NAMES,
7
8
  dottedToolName,
8
9
  namespacedToolName,
9
10
  normalizeDeclaredToolName,