@bitkyc08/opencodex 2.15.1 → 2.17.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 (41) hide show
  1. package/gui/dist/assets/{index-CMCDkQ7U.js → index-DOKr6RBR.js} +10 -10
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +2 -0
  5. package/src/adapters/google-antigravity-replay.ts +9 -1
  6. package/src/adapters/kiro-thinking.ts +8 -0
  7. package/src/adapters/kiro.ts +45 -42
  8. package/src/adapters/openai-chat.ts +5 -2
  9. package/src/adapters/openai-responses.ts +5 -1
  10. package/src/cli/dispatch.ts +6 -3
  11. package/src/cli/export-command.ts +19 -7
  12. package/src/cli/help.ts +1 -1
  13. package/src/cli/index.ts +1 -0
  14. package/src/cli/registry.ts +2 -2
  15. package/src/clients/config-export.ts +165 -3
  16. package/src/generated/compatibility-version.json +59 -31
  17. package/src/integrations/config-io.ts +119 -1
  18. package/src/integrations/omp-yaml-source.ts +232 -99
  19. package/src/integrations/registry.ts +14 -0
  20. package/src/integrations/serialize.ts +80 -1
  21. package/src/integrations/state.ts +38 -6
  22. package/src/integrations/writer-lock.ts +98 -0
  23. package/src/integrations/writer.ts +152 -19
  24. package/src/lab/automation/orchestrator.ts +19 -0
  25. package/src/lib/lab-activation.ts +161 -0
  26. package/src/lib/lab-passive-linker-registration.ts +26 -0
  27. package/src/lib/optional-shutdown-hooks.ts +57 -0
  28. package/src/lib/shadow-call.ts +6 -14
  29. package/src/lib/translator-budget.ts +34 -0
  30. package/src/providers/antigravity-models.ts +65 -10
  31. package/src/routing/compatibility/assemble.ts +21 -107
  32. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  33. package/src/routing/compatibility/provider-slot.ts +56 -0
  34. package/src/server/index.ts +8 -17
  35. package/src/server/lifecycle.ts +5 -3
  36. package/src/server/management/integration-routes.ts +21 -14
  37. package/src/server/management/routing-profile-routes.ts +9 -1
  38. package/src/server/management-api.ts +37 -6
  39. package/src/server/passive-route-linker.ts +66 -0
  40. package/src/server/responses/core.ts +20 -21
  41. package/src/types.ts +15 -5
@@ -19,13 +19,14 @@ import {
19
19
  import { readIntegrationState } from "../../integrations/state";
20
20
  import { createIntegrationStateStore, type IntegrationStateStore } from "../../integrations/store";
21
21
  import {
22
- applyIntegration,
23
- disableIntegration,
24
- restoreIntegration,
22
+ applyIntegrationCoordinated,
23
+ disableIntegrationCoordinated,
24
+ restoreIntegrationCoordinated,
25
25
  type IntegrationRestoreInput,
26
26
  type IntegrationWriteInput,
27
27
  type WriteRefused,
28
28
  } from "../../integrations/writer";
29
+ import { IntegrationWriterLockBusyError, type IntegrationWriterLockSeams } from "../../integrations/writer-lock";
29
30
  import { jsonResponse } from "../auth-cors";
30
31
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
31
32
  import type { ManagementContext } from "./context";
@@ -37,9 +38,9 @@ const INTEGRATION_MUTATION_JOIN_MS = 120_000;
37
38
  export const INTEGRATION_MUTATION_TERMINAL_MS = 10 * 60_000;
38
39
 
39
40
  type IntegrationStateRecord = Awaited<ReturnType<typeof readIntegrationState>>;
40
- type ApplyResult = Awaited<ReturnType<typeof applyIntegration>>;
41
- type DisableResult = Awaited<ReturnType<typeof disableIntegration>>;
42
- type RestoreResult = Awaited<ReturnType<typeof restoreIntegration>>;
41
+ type ApplyResult = Awaited<ReturnType<typeof applyIntegrationCoordinated>>;
42
+ type DisableResult = Awaited<ReturnType<typeof disableIntegrationCoordinated>>;
43
+ type RestoreResult = Awaited<ReturnType<typeof restoreIntegrationCoordinated>>;
43
44
 
44
45
  export type IntegrationStateEnvelope = {
45
46
  clientId: IntegrationClientId;
@@ -95,6 +96,7 @@ class IntegrationMutationBusyError extends Error {
95
96
  const integrationMutationFlights = new Map<IntegrationClientId, IntegrationMutationFlight>();
96
97
  let integrationMutationTestHooks: {
97
98
  io?: IntegrationIO;
99
+ lockSeams?: IntegrationWriterLockSeams;
98
100
  /**
99
101
  * Bind every read and write in the request to one store. Without this a
100
102
  * route test could isolate the writer but not the journal listing or the
@@ -185,6 +187,7 @@ function runIntegrationMutationFlight<T>(
185
187
  export function setIntegrationMutationFlightTestHooks(
186
188
  hooks: {
187
189
  io?: IntegrationIO;
190
+ lockSeams?: IntegrationWriterLockSeams;
188
191
  /** Binds the WHOLE request — reads, writes and journal — to one store. */
189
192
  store?: IntegrationStateStore;
190
193
  run?: (operation: () => Promise<unknown>) => Promise<unknown>;
@@ -433,6 +436,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
433
436
 
434
437
  const opId = parsed.opId.trim();
435
438
  const confirmDrift = parsed.confirmDrift ?? false;
439
+ let restoreClientId: IntegrationClientId | undefined;
436
440
  try {
437
441
  const store = integrationStore();
438
442
  const operation = store.findOperation(opId);
@@ -443,6 +447,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
443
447
  opId,
444
448
  }, 404, req, ctx.config);
445
449
  }
450
+ restoreClientId = operation.clientId;
446
451
  const snapshot = store.readSnapshot(operation);
447
452
  if (snapshot.kind === "expired") {
448
453
  return jsonResponse({
@@ -462,7 +467,9 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
462
467
  operation.clientId,
463
468
  `restore:${opId}:${confirmDrift}`,
464
469
  writeInput.io?.now ?? Date.now,
465
- () => Promise.resolve(restoreIntegration(restoreInput)),
470
+ () => restoreIntegrationCoordinated(restoreInput, {
471
+ lockSeams: integrationMutationTestHooks?.lockSeams,
472
+ }),
466
473
  );
467
474
  if (!result.ok) {
468
475
  /*
@@ -478,11 +485,11 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
478
485
  }
479
486
  return jsonResponse(result satisfies IntegrationRestoreEnvelope, 200, req, ctx.config);
480
487
  } catch (error) {
481
- if (error instanceof IntegrationMutationBusyError) {
488
+ if (error instanceof IntegrationMutationBusyError || error instanceof IntegrationWriterLockBusyError) {
482
489
  return jsonResponse({
483
490
  error: "integration mutation busy",
484
491
  code: "integration_mutation_busy",
485
- clientId: error.clientId,
492
+ clientId: restoreClientId ?? (error instanceof IntegrationMutationBusyError ? error.clientId : undefined),
486
493
  }, 409, req, ctx.config);
487
494
  }
488
495
  return internalErrorResponse(error, ctx);
@@ -519,18 +526,18 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
519
526
  requestedClient,
520
527
  parsed.enabled ? "apply" : "disable",
521
528
  input.io?.now ?? Date.now,
522
- () => Promise.resolve(parsed.enabled
523
- ? applyIntegration(input)
524
- : disableIntegration(input)),
529
+ () => parsed.enabled
530
+ ? applyIntegrationCoordinated(input, { lockSeams: integrationMutationTestHooks?.lockSeams })
531
+ : disableIntegrationCoordinated(input, { lockSeams: integrationMutationTestHooks?.lockSeams }),
525
532
  );
526
533
  if (!result.ok) return writerFailureResponse(requestedClient, result, ctx);
527
534
  return jsonResponse(result satisfies IntegrationToggleEnvelope, 200, req, ctx.config);
528
535
  } catch (error) {
529
- if (error instanceof IntegrationMutationBusyError) {
536
+ if (error instanceof IntegrationMutationBusyError || error instanceof IntegrationWriterLockBusyError) {
530
537
  return jsonResponse({
531
538
  error: "integration mutation busy",
532
539
  code: "integration_mutation_busy",
533
- clientId: error.clientId,
540
+ clientId: requestedClient,
534
541
  }, 409, req, ctx.config);
535
542
  }
536
543
  return internalErrorResponse(error, ctx);
@@ -17,9 +17,10 @@ import {
17
17
  } from "../../routing/profile";
18
18
  import { evaluatePolicyProfile, type PolicyCandidateEvidence, type PolicyRequestEvidence } from "../../routing/evaluator";
19
19
  import { assemblePolicyCandidateEvidence } from "../../routing/compatibility/assemble";
20
+ import { activateLab, labActivationRequired } from "../../lib/lab-activation";
20
21
  import { quotaEvidenceForCandidate } from "../../routing/quota";
21
22
  import { routedProviderConfig } from "../../router";
22
- import { saveConfigPreservingClaudeCode } from "../../config";
23
+ import { saveConfigPreservingClaudeCode, getConfigDir } from "../../config";
23
24
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
24
25
  import { isPlainRecord } from "./shared";
25
26
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -289,6 +290,9 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
289
290
  const nextProfiles = { ...(config.routingProfiles ?? {}) };
290
291
  nextProfiles[id] = storedProfile(id, body.profile as OcxRoutingProfileConfig);
291
292
  config.routingProfiles = nextProfiles;
293
+ // Creating the first profile on a process started profile-less must install the
294
+ // compatibility provider now; activation is synchronous and idempotent per configDir.
295
+ if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
292
296
  // An alias change on update renames the public model id; rewrite config
293
297
  // references (disabledModels, subagentModels, injectionModel,
294
298
  // shadowCallIntercept, claudeCode) so they follow the new alias.
@@ -358,6 +362,10 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
358
362
  // One clock read for both assembly and evaluation keeps freshness, health,
359
363
  // and trace timestamps mutually consistent with the production router.
360
364
  const now = Date.now();
365
+ // R3-1: dry-run assembles candidate evidence independently of the startup gate, so an
366
+ // operator preview on a process started without profiles would silently omit
367
+ // compatibility evidence and disagree with production. Activate first.
368
+ if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
361
369
  const candidateEvidence = body.candidates === undefined
362
370
  ? assembleCandidateEvidence(config, resolvedProfile, now)
363
371
  : parseCandidateEvidence(body.candidates);
@@ -61,15 +61,12 @@ import { handleConfigRoutes } from "./management/config-routes";
61
61
  import { handleLogsUsageRoutes } from "./management/logs-usage-routes";
62
62
  import { handleRequestHistoryRoutes } from "./management/request-history-routes";
63
63
  import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-routes";
64
- import { handleRoutingProfileRoutes } from "./management/routing-profile-routes";
65
64
  import { handleProviderRoutes } from "./management/provider-routes";
66
65
  import { handleModelRoutes } from "./management/model-routes";
67
66
  import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
68
67
  import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
69
68
  import { handleComboRoutes } from "./management/combo-routes";
70
69
  import { handleSystemRoutes } from "./management/system-routes";
71
- import { handleLabRoutes } from "./management/lab-routes";
72
- import { handleLabAutomationRoutes } from "./management/lab-automation-routes";
73
70
  import { handleSidebarRoutes } from "./management/sidebar-routes";
74
71
  import { handleIntegrationRoutes } from "./management/integration-routes";
75
72
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
@@ -96,6 +93,41 @@ const managementConvergenceBindings = new WeakMap<object, Readonly<{
96
93
  converge: ConvergeCodex;
97
94
  }>>();
98
95
 
96
+ /**
97
+ * Namespace match for management route prefixes: exact hit or a child path, never a
98
+ * prefix collision (`/api/labfoo` must not match `/api/lab`).
99
+ */
100
+ function pathInManagementNamespace(pathname: string, prefix: string): boolean {
101
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
102
+ }
103
+
104
+ /**
105
+ * Routing-profile and Compatibility Lab handlers statically import the Lab module graph,
106
+ * so mounting them eagerly would pull ~70 `src/lab/` modules into every management
107
+ * request -- including installs that never opted into Lab. Loading them per namespace
108
+ * keeps `management-api.ts` on the same footing as the three protected core files.
109
+ *
110
+ * Cherry-picked from @Wibias's PR #1676, which solved this before the boundary work
111
+ * reached it. See devlog/_plan/260814_lab_core_decoupling/.
112
+ */
113
+ async function handleRoutingProfileRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
114
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/routing-profiles")) return null;
115
+ const { handleRoutingProfileRoutes } = await import("./management/routing-profile-routes");
116
+ return handleRoutingProfileRoutes(ctx);
117
+ }
118
+
119
+ async function handleLabRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
120
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/lab")) return null;
121
+ // Automation is checked first so its narrower namespace keeps its own handler, matching
122
+ // the eager chain's ordering.
123
+ if (pathInManagementNamespace(ctx.url.pathname, "/api/lab/automation")) {
124
+ const { handleLabAutomationRoutes } = await import("./management/lab-automation-routes");
125
+ return handleLabAutomationRoutes(ctx);
126
+ }
127
+ const { handleLabRoutes } = await import("./management/lab-routes");
128
+ return handleLabRoutes(ctx);
129
+ }
130
+
99
131
  export async function handleManagementAPI(
100
132
  req: Request,
101
133
  url: URL,
@@ -180,7 +212,7 @@ export async function handleManagementAPI(
180
212
  ?? (await handleLogsUsageRoutes(ctx))
181
213
  ?? (await handleRequestHistoryRoutes(ctx))
182
214
  ?? (await handleRoutingAnalyticsRoutes(ctx))
183
- ?? (await handleRoutingProfileRoutes(ctx))
215
+ ?? (await handleRoutingProfileRoutesOnDemand(ctx))
184
216
  ?? (await handleProviderRoutes(ctx))
185
217
  ?? (await handleModelRoutes(ctx))
186
218
  ?? (await handleIntegrationRoutes(ctx))
@@ -189,8 +221,7 @@ export async function handleManagementAPI(
189
221
  ?? (await handleOauthAccountRoutes(ctx))
190
222
  ?? (await handleComboRoutes(ctx))
191
223
  ?? (await handleSystemRoutes(ctx))
192
- ?? (await handleLabAutomationRoutes(ctx))
193
- ?? (await handleLabRoutes(ctx))
224
+ ?? (await handleLabRoutesOnDemand(ctx))
194
225
  ?? (await handleSidebarRoutes(ctx));
195
226
  } catch (error) {
196
227
  const tooLarge = managementBodyTooLargeResponse(error, req, config);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Optional per-attempt route-identity linker.
3
+ *
4
+ * Compatibility Lab attaches an opaque route-subject digest to request attempts so its
5
+ * passive-production surface (CL-09) can correlate them later. That is an opt-in
6
+ * subsystem, so the core request path holds only a slot: null on installs that never
7
+ * activate Lab, which is every install without a routing profile.
8
+ *
9
+ * Contract for any registered implementation: synchronous, free of side effects with
10
+ * respect to the request, and non-throwing. The upstream request must never be delayed,
11
+ * retried, or altered by identity linkage. The try/catch lives here rather than at the
12
+ * call site so the guarantee belongs to the mechanism instead of being restated by every
13
+ * caller.
14
+ *
15
+ * See devlog/_plan/260814_lab_core_decoupling/020_request_path_gate.md
16
+ */
17
+ import type { OcxConfig, OcxProviderConfig } from "../types";
18
+ import type { InboundWire } from "../providers/registry";
19
+
20
+ export type PassiveRouteLinker = (
21
+ config: OcxConfig,
22
+ providerName: string,
23
+ modelId: string,
24
+ routed: OcxProviderConfig,
25
+ inboundWire: InboundWire,
26
+ ) => string | null;
27
+
28
+ let linker: PassiveRouteLinker | null = null;
29
+
30
+ /** Install the linker. Returns a detach function. */
31
+ export function setPassiveRouteLinker(next: PassiveRouteLinker): () => void {
32
+ linker = next;
33
+ return () => {
34
+ // Only detach our own registration: a later activation may have replaced it.
35
+ if (linker === next) linker = null;
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Resolve the attempt identity, or null when no subsystem is active.
41
+ * Never throws: linkage is best-effort metadata and must not affect the request.
42
+ */
43
+ export function resolvePassiveRouteSubjectId(
44
+ config: OcxConfig,
45
+ providerName: string,
46
+ modelId: string,
47
+ routed: OcxProviderConfig,
48
+ inboundWire: InboundWire,
49
+ ): string | null {
50
+ if (!linker) return null;
51
+ try {
52
+ return linker(config, providerName, modelId, routed, inboundWire);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** True when an optional subsystem has installed a linker. Test/diagnostic use. */
59
+ export function hasPassiveRouteLinker(): boolean {
60
+ return linker !== null;
61
+ }
62
+
63
+ /** Test-only reset. */
64
+ export function resetPassiveRouteLinkerForTests(): void {
65
+ linker = null;
66
+ }
@@ -33,7 +33,7 @@ import {
33
33
  type RouteResult,
34
34
  } from "../../router";
35
35
  import { evidenceFromBody } from "../../routing/request-evidence";
36
- import { resolveProductionRouteSubject } from "../../routing/compatibility/subject";
36
+ import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
37
37
  import {
38
38
  advanceComboAfterFailure,
39
39
  comboDefaultEffort,
@@ -228,7 +228,7 @@ import {
228
228
  payloadRewriteAsBlockRewrite,
229
229
  relaySseWithBlockRewrite,
230
230
  } from "../sse-payload-rewrite";
231
- import { collectRoutedCustomToolNames, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
231
+ import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
232
232
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
233
233
  import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
234
234
  import { responsesJsonToSseStream } from "../responses-json-events";
@@ -1582,7 +1582,6 @@ async function handleResponsesInner(
1582
1582
  if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
1583
1583
  parsed.modelId,
1584
1584
  _sci.sourceModels,
1585
- req.headers,
1586
1585
  )) {
1587
1586
  const _sciOriginal = parsed.modelId;
1588
1587
  parsed.modelId = _sci.model;
@@ -1991,22 +1990,19 @@ async function handleResponsesInner(
1991
1990
  (logCtx.attempts ??= []).push(attempt);
1992
1991
  }
1993
1992
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
1994
- // CL-09: attach only the opaque exact route-subject identity to the attempt.
1995
- // This is best-effort passive metadata: no Lab state is created and failure
1996
- // must never alter, retry, or delay the upstream request.
1993
+ // Optional route-identity linkage for attempt correlation (CL-09 consumes it). The slot
1994
+ // resolves to null unless an opt-in subsystem registered a linker, so an install without
1995
+ // routing profiles does no work here and loads no additional module. The non-throwing
1996
+ // guarantee lives in the slot helper.
1997
1997
  if (logCtx.activeAttempt && !logCtx.activeAttempt.labRouteSubjectId) {
1998
- try {
1999
- const passiveSubject = resolveProductionRouteSubject(
2000
- config,
2001
- route.providerName,
2002
- route.modelId,
2003
- route.provider,
2004
- inboundWire,
2005
- );
2006
- if (passiveSubject) logCtx.activeAttempt.labRouteSubjectId = passiveSubject.subjectId;
2007
- } catch {
2008
- // Omit passive linkage when exact subject construction is unavailable.
2009
- }
1998
+ const passiveSubjectId = resolvePassiveRouteSubjectId(
1999
+ config,
2000
+ route.providerName,
2001
+ route.modelId,
2002
+ route.provider,
2003
+ inboundWire,
2004
+ );
2005
+ if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId;
2010
2006
  }
2011
2007
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
2012
2008
 
@@ -2130,9 +2126,7 @@ async function handleResponsesInner(
2130
2126
  const imageGenCallAliases = route.provider.authMode === "forward"
2131
2127
  ? new Map<string, { namespace: string; name: string }>()
2132
2128
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
2133
- const routedCustomToolNames = route.provider.authMode === "forward"
2134
- ? new Set<string>()
2135
- : collectRoutedCustomToolNames(parsed._rawBody);
2129
+ const routedCustomToolNames = new Set<string>();
2136
2130
  // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
2137
2131
  // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
2138
2132
  // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
@@ -2161,6 +2155,11 @@ async function handleResponsesInner(
2161
2155
  releaseCodexAuthContextProbeLease(authCtx);
2162
2156
  throw error;
2163
2157
  }
2158
+ if (route.provider.authMode !== "forward") {
2159
+ for (const name of request.convertedRoutedCustomToolNames ?? []) {
2160
+ if (toolBridgeMaps.freeformToolNames.has(name)) routedCustomToolNames.add(name);
2161
+ }
2162
+ }
2164
2163
  recordAdapterReasoning(logCtx, request);
2165
2164
  const actualHostKey = upstreamHostHealthKey(
2166
2165
  route.providerName,
package/src/types.ts CHANGED
@@ -756,11 +756,11 @@ export interface OcxConfig {
756
756
  */
757
757
  customModelCatalogMigration?: unknown;
758
758
  /**
759
- * Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
760
- * commit messages, skill orchestration) to a user-chosen model. Default intercepted
761
- * source models: gpt-5.4-mini (older clients) and gpt-5.6-luna (Codex 0.145.0+).
762
- * Opt-in; disabled by default. Matching maintenance/helper requests are forced to low.
763
- * Normal Codex turns identified by request_kind=turn are never rewritten.
759
+ * Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
760
+ * commit messages, skill orchestration) to a user-chosen model. Default intercepted
761
+ * source models: gpt-5.4-mini (older clients) and gpt-5.6-luna (Codex 0.145.0+).
762
+ * Opt-in; disabled by default. Matching maintenance/helper requests are forced to low.
763
+ * All requests for configured shadow source models are intercepted unconditionally.
764
764
  */
765
765
  shadowCallIntercept?: {
766
766
  /** When true, requests for known shadow/helper source models are rewritten to the configured model. */
@@ -1450,6 +1450,16 @@ export interface OcxProviderConfig {
1450
1450
  * only on explicit `true`. See devlog/_plan/260709_parallel_tool_calls.
1451
1451
  */
1452
1452
  parallelToolCalls?: boolean;
1453
+ /**
1454
+ * Opt-in: when `parallelToolCalls` is `false`, actually send `parallel_tool_calls: false`
1455
+ * on the `/chat/completions` wire for this provider. By default an opted-out provider only
1456
+ * OMITS the field (strict OpenAI-compatible hosts reject unknown knobs), and the NVIDIA NIM
1457
+ * baseUrl is the sole built-in exception that pins the wire bit. Some self-hosted gateways
1458
+ * (Kimi/GLM-family, vLLM, etc.) do honor `parallel_tool_calls` and keep emitting concurrent
1459
+ * tool calls unless it is present; enable this to pin the bit without hardcoding their URL.
1460
+ * No effect unless `parallelToolCalls === false`; ignored by non-`openai-chat` adapters.
1461
+ */
1462
+ pinParallelToolCallsFalse?: boolean;
1453
1463
  /**
1454
1464
  * Opt-in: forward `prompt_cache_key` to the upstream `/chat/completions` body.
1455
1465
  * OpenAI-specific extension; strict backends (Groq, Cerebras, etc.) reject unknown