@bitkyc08/opencodex 2.15.1-preview.20260814 → 2.16.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 (40) hide show
  1. package/gui/dist/assets/{index-1U3HI8uT.js → index-CZwbOse7.js} +1 -1
  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/cursor/effort-map.ts +4 -5
  6. package/src/adapters/cursor/request-builder.ts +1 -1
  7. package/src/adapters/google-antigravity-replay.ts +9 -1
  8. package/src/adapters/kiro-thinking.ts +8 -0
  9. package/src/adapters/kiro.ts +45 -42
  10. package/src/adapters/openai-chat.ts +5 -2
  11. package/src/adapters/openai-responses.ts +5 -1
  12. package/src/cli/dispatch.ts +6 -3
  13. package/src/cli/index.ts +1 -0
  14. package/src/generated/compatibility-version.json +56 -32
  15. package/src/generated/model-metadata.ts +1 -1
  16. package/src/integrations/config-io.ts +119 -1
  17. package/src/integrations/omp-yaml-source.ts +6 -1
  18. package/src/integrations/serialize.ts +80 -1
  19. package/src/integrations/state.ts +37 -6
  20. package/src/integrations/writer.ts +11 -3
  21. package/src/lab/automation/orchestrator.ts +19 -0
  22. package/src/lib/lab-activation.ts +161 -0
  23. package/src/lib/lab-passive-linker-registration.ts +26 -0
  24. package/src/lib/optional-shutdown-hooks.ts +57 -0
  25. package/src/lib/translator-budget.ts +34 -0
  26. package/src/oauth/index.ts +3 -0
  27. package/src/providers/antigravity-models.ts +156 -36
  28. package/src/providers/model-rename-migration.ts +54 -1
  29. package/src/providers/registry.ts +1 -1
  30. package/src/routing/compatibility/assemble.ts +21 -107
  31. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  32. package/src/routing/compatibility/provider-slot.ts +56 -0
  33. package/src/server/index.ts +8 -17
  34. package/src/server/lifecycle.ts +5 -3
  35. package/src/server/management/routing-profile-routes.ts +9 -1
  36. package/src/server/management-api.ts +37 -6
  37. package/src/server/passive-route-linker.ts +66 -0
  38. package/src/server/responses/core.ts +20 -20
  39. package/src/types.ts +10 -0
  40. package/src/usage/expected-prices.ts +13 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Compatibility-evidence provider (Lab-backed).
3
+ *
4
+ * Holds every Lab-reaching part of policy candidate evidence: route/protocol subject
5
+ * construction, the suite catalog snapshot, and the projection read. The core assembler
6
+ * (`assemble.ts`) keeps capability, health, quota, and cost and consults this only through
7
+ * the provider slot, so an install that never activates Lab never loads this module.
8
+ *
9
+ * This is a relocation of previously inline logic, not a rewrite: `attachCompatibilityEvidence`
10
+ * below is the original function, and its state arrives entirely through arguments.
11
+ *
12
+ * @internal registered by the Lab activation path
13
+ */
14
+ import type { OcxConfig } from "../../types";
15
+ import type { NormalizedRoutingProfile } from "../profile";
16
+ import {
17
+ compatibilitySuiteKey,
18
+ loadCompatibilityCatalogSnapshot,
19
+ type CompatibilityCatalogSnapshot,
20
+ } from "./catalog";
21
+ import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
22
+ import {
23
+ resolvePolicyCompatibilitySubjects,
24
+ type ResolvedPolicyCompatibilitySubjects,
25
+ } from "./subject";
26
+ import type { CandidateCompatibilityEvidence } from "./types";
27
+ import type { CoreEvidenceOptions, CompatibilityEvidenceProvider } from "./provider-slot";
28
+
29
+ /** Lab-side seams, kept off the core options contract. */
30
+ export interface LabCompatibilityProviderOptions extends CoreEvidenceOptions {
31
+ resolveSubjects?: typeof resolvePolicyCompatibilitySubjects;
32
+ loadEvidenceSnapshot?: typeof loadCompatibilityEvidenceSnapshot;
33
+ loadCatalogSnapshot?: typeof loadCompatibilityCatalogSnapshot;
34
+ }
35
+
36
+ function attachCompatibilityEvidence(
37
+ resolved: ResolvedPolicyCompatibilitySubjects | undefined,
38
+ snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
39
+ catalog: CompatibilityCatalogSnapshot,
40
+ profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
41
+ ): CandidateCompatibilityEvidence {
42
+ const subjectIds = resolved?.subjectIds ?? {};
43
+ const suites: CandidateCompatibilityEvidence["suites"] = [];
44
+
45
+ for (const requirement of profile.requiredSuites) {
46
+ const subjectId = subjectIds[requirement.evidenceLayer];
47
+ if (!subjectId) continue;
48
+ const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
49
+ if (!metadata) continue;
50
+ const row = findVerdictForSuite(
51
+ snapshot,
52
+ subjectId,
53
+ requirement.evidenceLayer,
54
+ requirement.suiteId,
55
+ metadata.suiteVersion,
56
+ metadata.suiteManifestDigest,
57
+ );
58
+ if (!row) continue;
59
+ suites.push({
60
+ subjectId,
61
+ suiteId: row.suiteId,
62
+ evidenceLayer: requirement.evidenceLayer,
63
+ suiteVersion: row.suiteVersion,
64
+ suiteManifestDigest: row.suiteManifestDigest,
65
+ verdict: row.verdict,
66
+ asOf: row.asOf,
67
+ maxAgeMs: metadata.maxAgeMs,
68
+ notes: row.notes,
69
+ });
70
+ }
71
+
72
+ return {
73
+ subjectIds: { ...subjectIds },
74
+ projectionAvailable: snapshot.projectionAvailable,
75
+ suites,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Build compatibility evidence for every candidate of one profile.
81
+ * Keyed `provider/model`; a candidate absent from the map has no compatibility evidence.
82
+ */
83
+ export const labCompatibilityEvidenceProvider: CompatibilityEvidenceProvider = (
84
+ config: OcxConfig,
85
+ profile: NormalizedRoutingProfile,
86
+ policy: NonNullable<NormalizedRoutingProfile["compatibility"]>,
87
+ options: CoreEvidenceOptions,
88
+ ): Map<string, CandidateCompatibilityEvidence> => {
89
+ const labOptions = options as LabCompatibilityProviderOptions;
90
+ const resolveSubjects = labOptions.resolveSubjects ?? resolvePolicyCompatibilitySubjects;
91
+ const loadCatalog = labOptions.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
92
+ const loadEvidence = labOptions.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
93
+
94
+ const resolvedByCandidate = new Map<string, ResolvedPolicyCompatibilitySubjects>();
95
+ const catalog: CompatibilityCatalogSnapshot = loadCatalog(policy.requiredSuites);
96
+ const subjectIds = new Set<string>();
97
+
98
+ for (const candidate of profile.candidates) {
99
+ const provider = config.providers[candidate.provider];
100
+ if (!provider) continue;
101
+ try {
102
+ const routed = options.routedProviderConfig(candidate.provider, provider);
103
+ const resolved = resolveSubjects(
104
+ config,
105
+ candidate.provider,
106
+ candidate.model,
107
+ routed,
108
+ options.configDir,
109
+ );
110
+ resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
111
+ for (const subjectId of Object.values(resolved.subjectIds)) {
112
+ if (subjectId) subjectIds.add(subjectId);
113
+ }
114
+ } catch {
115
+ // Subject construction failure is handled per required layer as unknown.
116
+ }
117
+ }
118
+
119
+ const snapshot = loadEvidence([...subjectIds], options.configDir);
120
+
121
+ const byCandidate = new Map<string, CandidateCompatibilityEvidence>();
122
+ for (const candidate of profile.candidates) {
123
+ const key = `${candidate.provider}/${candidate.model}`;
124
+ byCandidate.set(
125
+ key,
126
+ attachCompatibilityEvidence(resolvedByCandidate.get(key), snapshot, catalog, policy),
127
+ );
128
+ }
129
+ return byCandidate;
130
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Slot for the optional compatibility-evidence provider.
3
+ *
4
+ * Routing is synchronous and must stay synchronous: `routeModelInternal` is sync, and so
5
+ * are the subagent-fallback helpers that call `routeModel` (`isNativeModelQuotaExhausted`,
6
+ * `isModelHealthBlocked`, `selectAvailableSubagentModel`, ...). Making the chain async to
7
+ * permit a dynamic import would touch hundreds of call sites and break those APIs, so this
8
+ * is a plain nullable reference rather than an `await import()`.
9
+ *
10
+ * The Lab implementation is installed during activation. Installs without
11
+ * compatibility-gated routing profiles never register one, so the core evidence assembler
12
+ * never reaches the Lab module graph.
13
+ *
14
+ * See devlog/_plan/260814_lab_core_decoupling/030_router_and_startup_activation.md
15
+ */
16
+ import type { OcxConfig } from "../../types";
17
+ import type { NormalizedRoutingProfile } from "../profile";
18
+ import type { CandidateCompatibilityEvidence } from "./types";
19
+
20
+ /** Options the core assembler can supply without knowing anything Lab-specific. */
21
+ export interface CoreEvidenceOptions {
22
+ configDir?: string;
23
+ routedProviderConfig: (providerName: string, provider: import("../../types").OcxProviderConfig)
24
+ => import("../../types").OcxProviderConfig;
25
+ }
26
+
27
+ /**
28
+ * Produce compatibility evidence per candidate, keyed `provider/model`.
29
+ * A candidate absent from the map has no compatibility evidence.
30
+ */
31
+ export type CompatibilityEvidenceProvider = (
32
+ config: OcxConfig,
33
+ profile: NormalizedRoutingProfile,
34
+ policy: NonNullable<NormalizedRoutingProfile["compatibility"]>,
35
+ options: CoreEvidenceOptions,
36
+ ) => Map<string, CandidateCompatibilityEvidence>;
37
+
38
+ let provider: CompatibilityEvidenceProvider | null = null;
39
+
40
+ /** Install the provider. Returns a detach function. */
41
+ export function setCompatibilityEvidenceProvider(next: CompatibilityEvidenceProvider): () => void {
42
+ provider = next;
43
+ return () => {
44
+ if (provider === next) provider = null;
45
+ };
46
+ }
47
+
48
+ /** The installed provider, or null when no optional subsystem is active. */
49
+ export function resolveCompatibilityEvidenceProvider(): CompatibilityEvidenceProvider | null {
50
+ return provider;
51
+ }
52
+
53
+ /** Test-only reset. */
54
+ export function resetCompatibilityEvidenceProviderForTests(): void {
55
+ provider = null;
56
+ }
@@ -45,12 +45,7 @@ import {
45
45
  registerDefaultAppOwnedObservedBuffers,
46
46
  } from "../lib/app-owned-memory-stores";
47
47
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
48
- import {
49
- setLabAutomationDispatchDeps,
50
- startLabAutomationScheduler,
51
- } from "../lab/automation/orchestrator";
52
- import { loadLabAutomationPolicy } from "../lab/automation/persistence";
53
- import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
48
+ import { activateLab, labActivationRequired } from "../lib/lab-activation";
54
49
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
55
50
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
56
51
  import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
@@ -1735,18 +1730,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1735
1730
  // Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
1736
1731
  backgroundLifecycle.scheduleStartupRun();
1737
1732
 
1733
+ // Compatibility Lab is optional: wire it only for installs that actually use it -- any
1734
+ // routing profile, or automation enabled on disk. This runs synchronously before
1735
+ // startServer returns, in the same turn as Bun.serve, so a policy route can never be
1736
+ // evaluated before its evidence provider is registered. That ordering is load-bearing:
1737
+ // the subagent-fallback chain routes synchronously and has nowhere to await.
1738
1738
  const labConfigDir = getConfigDir();
1739
- const productionLabRouteExecutor = createProductionLabRouteExecutor({
1740
- configDir: labConfigDir,
1741
- loadConfig: () => config,
1742
- });
1743
- setLabAutomationDispatchDeps({
1744
- configDir: labConfigDir,
1745
- loadConfig: () => config,
1746
- routeExecutor: productionLabRouteExecutor,
1747
- });
1748
- if (loadLabAutomationPolicy(labConfigDir).enabled) {
1749
- startLabAutomationScheduler(labConfigDir);
1739
+ if (labActivationRequired(config, labConfigDir)) {
1740
+ activateLab(config, labConfigDir);
1750
1741
  }
1751
1742
 
1752
1743
  return server;
@@ -7,7 +7,7 @@ import {
7
7
  } from "../storage/policy-job";
8
8
  import { abortRestoreTrashJobAsync } from "../storage/restore-job";
9
9
  import { stopStorageCleanupScheduler } from "../storage/policy-scheduler";
10
- import { stopLabAutomationScheduler, requestLabAutomationShutdown } from "../lab/automation/orchestrator";
10
+ import { runOptionalShutdownHooks } from "../lib/optional-shutdown-hooks";
11
11
  import { stopStateStoreSweeper } from "../lib/state-store-sweeper";
12
12
  import {
13
13
  cancelQueuedStorageWorkerSpawns,
@@ -452,8 +452,10 @@ export async function drainAndShutdown(
452
452
  // Abort each job independently so one wedged join cannot skip the other,
453
453
  // then drain leftovers; failures must not prevent `server.stop`.
454
454
  stopStorageCleanupScheduler();
455
- requestLabAutomationShutdown();
456
- stopLabAutomationScheduler();
455
+ // Optional subsystems (Compatibility Lab today, anything added later) tear themselves
456
+ // down through hooks registered at activation. A process that never activated one runs
457
+ // nothing here and never loads its module graph.
458
+ runOptionalShutdownHooks();
457
459
  stopStateStoreSweeper();
458
460
  // The overlay reconciler is owner-scoped: the startServer stop override
459
461
  // releases THIS server's lease through runListenerShutdown →
@@ -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";
@@ -1991,22 +1991,19 @@ async function handleResponsesInner(
1991
1991
  (logCtx.attempts ??= []).push(attempt);
1992
1992
  }
1993
1993
  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.
1994
+ // Optional route-identity linkage for attempt correlation (CL-09 consumes it). The slot
1995
+ // resolves to null unless an opt-in subsystem registered a linker, so an install without
1996
+ // routing profiles does no work here and loads no additional module. The non-throwing
1997
+ // guarantee lives in the slot helper.
1997
1998
  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
- }
1999
+ const passiveSubjectId = resolvePassiveRouteSubjectId(
2000
+ config,
2001
+ route.providerName,
2002
+ route.modelId,
2003
+ route.provider,
2004
+ inboundWire,
2005
+ );
2006
+ if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId;
2010
2007
  }
2011
2008
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
2012
2009
 
@@ -2130,9 +2127,7 @@ async function handleResponsesInner(
2130
2127
  const imageGenCallAliases = route.provider.authMode === "forward"
2131
2128
  ? new Map<string, { namespace: string; name: string }>()
2132
2129
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
2133
- const routedCustomToolNames = route.provider.authMode === "forward"
2134
- ? new Set<string>()
2135
- : collectRoutedCustomToolNames(parsed._rawBody);
2130
+ const routedCustomToolNames = new Set<string>();
2136
2131
  // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
2137
2132
  // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
2138
2133
  // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
@@ -2161,6 +2156,11 @@ async function handleResponsesInner(
2161
2156
  releaseCodexAuthContextProbeLease(authCtx);
2162
2157
  throw error;
2163
2158
  }
2159
+ if (route.provider.authMode !== "forward") {
2160
+ for (const name of request.convertedRoutedCustomToolNames ?? []) {
2161
+ if (toolBridgeMaps.freeformToolNames.has(name)) routedCustomToolNames.add(name);
2162
+ }
2163
+ }
2164
2164
  recordAdapterReasoning(logCtx, request);
2165
2165
  const actualHostKey = upstreamHostHealthKey(
2166
2166
  route.providerName,
package/src/types.ts CHANGED
@@ -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
@@ -52,6 +52,10 @@ const DAYBREAK_RED: Cost4 = { input: 12.5, output: 75, cacheRead: 1.25, cacheWri
52
52
  const GPT56_TERRA: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 };
53
53
  const GPT56_LUNA: Cost4 = { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 };
54
54
  const GEMINI_36_FLASH: Cost4 = { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 };
55
+ // Gemini 3.7 Flash launch promotion: Google publishes $0.75 in / $3.75 out per 1M
56
+ // through 2026-12-31, stepping up to $1.50 / $7.50 on 2027-01-01. Revisit this row
57
+ // then — the promotional rate is dated on the pricing page, not open-ended.
58
+ const GEMINI_37_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 };
55
59
  const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 };
56
60
  const KIMI_K3: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 };
57
61
  const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 };
@@ -70,6 +74,7 @@ const CLAUDE_OPUS_5_DERIVED_SOURCE =
70
74
  const ANTHROPIC_PRICING = "https://platform.claude.com/docs/en/about-claude/pricing (official; 5m cache-write tier)";
71
75
 
72
76
  const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token";
77
+ const GEMINI_37_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-08-14); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token";
73
78
  const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo";
74
79
  const OPENAI_GPT56_PRICING = "https://developers.openai.com/api/docs/pricing";
75
80
  const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
@@ -102,6 +107,12 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
102
107
  // Google Antigravity effort-suffix variants — derived from the verified base-model
103
108
  // price (Google does not publish per-suffix prices; Agent inference bills at the
104
109
  // base model's standard rate per the official Billing FAQ).
110
+ // 3.7 Flash rides CCA, whose billing equivalence to the Developer API list price is
111
+ // not published, so this is `verified-derived` rather than `verified`: the number is
112
+ // proven, the claim that Antigravity charges it is inferred.
113
+ { provider: "google-antigravity", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: `derived: Gemini 3.7 Flash promotional rate through 2026-12-31 ${GEMINI_37_PRICING}`, verifiedAt: "2026-08-14", status: "verified-derived" },
114
+ // Retained after the 3.6 retirement: historical usage.jsonl rows still carry these
115
+ // ids, and dropping the row would silently zero the cost of requests already made.
105
116
  { provider: "google-antigravity", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
106
117
  { provider: "google-antigravity", modelId: "gemini-3.1-pro", cost4: GEMINI_31_PRO, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
107
118
  // OpenAI GPT-5.6 `-pro` virtual selections. The virtual resolver keeps the SELECTED id in
@@ -130,6 +141,8 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
130
141
  { provider: "google-antigravity", modelId: "gemini-3-flash-agent", cost4: GEMINI_36_FLASH, source: `compat alias -> gemini-3.6-flash-high ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified-derived" },
131
142
  // Direct Google Gemini API current model (verified — published table).
132
143
  { provider: "google", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: GEMINI_PRICING, verifiedAt: "2026-07-22", status: "verified" },
144
+ // Developer API row: the price IS published for this surface, so `verified`.
145
+ { provider: "google", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: GEMINI_37_PRICING, verifiedAt: "2026-08-14", status: "verified" },
133
146
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" },
134
147
  // Antigravity-bundled third-party models — derived from the underlying vendor's
135
148
  // official API price (Antigravity itself bills via subscription quota).