@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.
- package/gui/dist/assets/{index-CMCDkQ7U.js → index-DOKr6RBR.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/google-antigravity-replay.ts +9 -1
- package/src/adapters/kiro-thinking.ts +8 -0
- package/src/adapters/kiro.ts +45 -42
- package/src/adapters/openai-chat.ts +5 -2
- package/src/adapters/openai-responses.ts +5 -1
- package/src/cli/dispatch.ts +6 -3
- package/src/cli/export-command.ts +19 -7
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +165 -3
- package/src/generated/compatibility-version.json +59 -31
- package/src/integrations/config-io.ts +119 -1
- package/src/integrations/omp-yaml-source.ts +232 -99
- package/src/integrations/registry.ts +14 -0
- package/src/integrations/serialize.ts +80 -1
- package/src/integrations/state.ts +38 -6
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +152 -19
- package/src/lab/automation/orchestrator.ts +19 -0
- package/src/lib/lab-activation.ts +161 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/shadow-call.ts +6 -14
- package/src/lib/translator-budget.ts +34 -0
- package/src/providers/antigravity-models.ts +65 -10
- package/src/routing/compatibility/assemble.ts +21 -107
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/server/index.ts +8 -17
- package/src/server/lifecycle.ts +5 -3
- package/src/server/management/integration-routes.ts +21 -14
- package/src/server/management/routing-profile-routes.ts +9 -1
- package/src/server/management-api.ts +37 -6
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/responses/core.ts +20 -21
- 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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
41
|
-
type DisableResult = Awaited<ReturnType<typeof
|
|
42
|
-
type RestoreResult = Awaited<ReturnType<typeof
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
523
|
-
?
|
|
524
|
-
:
|
|
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:
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
//
|
|
1995
|
-
//
|
|
1996
|
-
//
|
|
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
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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 =
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
*
|
|
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
|