@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.
- package/gui/dist/assets/{index-1U3HI8uT.js → index-CZwbOse7.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/cursor/effort-map.ts +4 -5
- package/src/adapters/cursor/request-builder.ts +1 -1
- 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/index.ts +1 -0
- package/src/generated/compatibility-version.json +56 -32
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/config-io.ts +119 -1
- package/src/integrations/omp-yaml-source.ts +6 -1
- package/src/integrations/serialize.ts +80 -1
- package/src/integrations/state.ts +37 -6
- package/src/integrations/writer.ts +11 -3
- 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/translator-budget.ts +34 -0
- package/src/oauth/index.ts +3 -0
- package/src/providers/antigravity-models.ts +156 -36
- package/src/providers/model-rename-migration.ts +54 -1
- package/src/providers/registry.ts +1 -1
- 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/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 -20
- package/src/types.ts +10 -0
- 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
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
1740
|
-
|
|
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;
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
456
|
-
|
|
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
|
|
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";
|
|
@@ -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
|
-
//
|
|
1995
|
-
//
|
|
1996
|
-
//
|
|
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
|
-
|
|
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
|
-
}
|
|
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 =
|
|
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).
|