@bitkyc08/opencodex 2.15.1 → 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-CMCDkQ7U.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/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 +48 -24
- 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/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/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
|
@@ -6,8 +6,8 @@ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./mode
|
|
|
6
6
|
// CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
|
|
7
7
|
// and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
|
|
8
8
|
// receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
9
|
-
// picker exposes collapsed base models only when CCA returns every known tier;
|
|
10
|
-
// returned wire
|
|
9
|
+
// picker exposes collapsed known base models only when CCA returns every known tier; unknown
|
|
10
|
+
// returned wire ids remain visible so they stay directly routable.
|
|
11
11
|
|
|
12
12
|
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
13
13
|
|
|
@@ -63,6 +63,41 @@ const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.en
|
|
|
63
63
|
return out;
|
|
64
64
|
}, {});
|
|
65
65
|
|
|
66
|
+
const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
|
|
67
|
+
|
|
68
|
+
function pickerModelIdForDiscoveredWireId(
|
|
69
|
+
wireId: string,
|
|
70
|
+
available: ReadonlyMap<string, Record<string, unknown>>,
|
|
71
|
+
): string {
|
|
72
|
+
const explicitPickerId = Object.hasOwn(ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID, wireId)
|
|
73
|
+
? ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId]
|
|
74
|
+
: undefined;
|
|
75
|
+
if (explicitPickerId) {
|
|
76
|
+
const requiredWireIds = Object.hasOwn(ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL, explicitPickerId)
|
|
77
|
+
? ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[explicitPickerId] ?? []
|
|
78
|
+
: [];
|
|
79
|
+
if (requiredWireIds.every(id => available.has(id))) return explicitPickerId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// CCA uses a single `-tiered` row for models whose effort levels ride on the
|
|
83
|
+
// request's thinkingLevel field. Keep this generic so new tiered models do not
|
|
84
|
+
// require another provider-specific ID mapping.
|
|
85
|
+
if (wireId.endsWith("-tiered")) {
|
|
86
|
+
const baseId = wireId.slice(0, -"-tiered".length);
|
|
87
|
+
if (isKnownAntigravityPickerModelId(baseId)) return baseId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const effortMatch = /^(.*)-(low|medium|high)$/.exec(wireId);
|
|
91
|
+
if (effortMatch) {
|
|
92
|
+
const baseId = effortMatch[1]!;
|
|
93
|
+
if (isKnownAntigravityPickerModelId(baseId)
|
|
94
|
+
&& ANTIGRAVITY_DISCOVERY_EFFORTS.every(effort => available.has(`${baseId}-${effort}`))) {
|
|
95
|
+
return baseId;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return wireId;
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
// ── Effort ladders per collapsed base model ──
|
|
67
102
|
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
68
103
|
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
@@ -141,6 +176,10 @@ export const ANTIGRAVITY_MODELS = [
|
|
|
141
176
|
"gpt-oss-120b-medium",
|
|
142
177
|
];
|
|
143
178
|
|
|
179
|
+
function isKnownAntigravityPickerModelId(value: string): boolean {
|
|
180
|
+
return isValidModelDiscoveryModelId(value) && ANTIGRAVITY_MODELS.includes(value);
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
145
184
|
const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
146
185
|
"gemini-3.7-flash": 1_048_576,
|
|
@@ -222,6 +261,7 @@ export function parseAntigravityAvailableModels(
|
|
|
222
261
|
if (!Array.isArray(modelIds)) return null;
|
|
223
262
|
for (const id of modelIds) {
|
|
224
263
|
if (!isValidModelDiscoveryModelId(id)
|
|
264
|
+
|| !Object.hasOwn(models, id)
|
|
225
265
|
|| !antigravityRecord(models[id])
|
|
226
266
|
|| ids.length >= limit) return null;
|
|
227
267
|
ids.push(id);
|
|
@@ -235,6 +275,19 @@ export function parseAntigravityAvailableModels(
|
|
|
235
275
|
if (ids.length >= limit) return null;
|
|
236
276
|
ids.push("gemini-3.1-flash-image");
|
|
237
277
|
}
|
|
278
|
+
// Newer CCA responses identify tiered Flash models through this index instead of
|
|
279
|
+
// adding their synthetic wire ids to agentModelSorts.
|
|
280
|
+
const tieredModelIds = antigravityRecord(body.tieredModelIds);
|
|
281
|
+
const flashTieredIds = tieredModelIds?.flash;
|
|
282
|
+
if (Array.isArray(flashTieredIds)) {
|
|
283
|
+
for (const id of flashTieredIds) {
|
|
284
|
+
if (!isValidModelDiscoveryModelId(id)
|
|
285
|
+
|| !Object.hasOwn(models, id)
|
|
286
|
+
|| !antigravityRecord(models[id])
|
|
287
|
+
|| ids.length >= limit) return null;
|
|
288
|
+
ids.push(id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
238
291
|
|
|
239
292
|
const available = new Map<string, Record<string, unknown>>();
|
|
240
293
|
for (const wireId of ids) {
|
|
@@ -242,17 +295,17 @@ export function parseAntigravityAvailableModels(
|
|
|
242
295
|
if (!info || available.has(wireId)) continue;
|
|
243
296
|
// Legacy compatibility aliases are deliberately routed to newer wire ids for saved
|
|
244
297
|
// selections. They are not safe as independently discovered picker rows.
|
|
245
|
-
|
|
298
|
+
const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, wireId)
|
|
299
|
+
? ANTIGRAVITY_MODEL_ALIASES[wireId]
|
|
300
|
+
: undefined;
|
|
301
|
+
if (alias && alias !== wireId) continue;
|
|
246
302
|
available.set(wireId, info);
|
|
247
303
|
}
|
|
248
304
|
|
|
249
305
|
const out: AntigravityAvailableModel[] = [];
|
|
250
306
|
const seen = new Set<string>();
|
|
251
307
|
for (const [wireId, info] of available) {
|
|
252
|
-
const
|
|
253
|
-
const completePickerSet = pickerId !== undefined
|
|
254
|
-
&& ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[pickerId]!.every(id => available.has(id));
|
|
255
|
-
const id = completePickerSet ? pickerId! : wireId;
|
|
308
|
+
const id = pickerModelIdForDiscoveredWireId(wireId, available);
|
|
256
309
|
if (seen.has(id)) continue;
|
|
257
310
|
seen.add(id);
|
|
258
311
|
out.push({
|
|
@@ -265,7 +318,9 @@ export function parseAntigravityAvailableModels(
|
|
|
265
318
|
}
|
|
266
319
|
|
|
267
320
|
export function resolveAntigravityWireModelId(modelId: string): string {
|
|
268
|
-
return ANTIGRAVITY_MODEL_ALIASES
|
|
321
|
+
return Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, modelId)
|
|
322
|
+
? ANTIGRAVITY_MODEL_ALIASES[modelId]
|
|
323
|
+
: modelId;
|
|
269
324
|
}
|
|
270
325
|
|
|
271
326
|
/**
|
|
@@ -279,7 +334,7 @@ export function isAntigravitySuffixModelId(modelId: string): boolean {
|
|
|
279
334
|
|
|
280
335
|
/** The reasoning tier a retired Flash id used to encode, if it is one. */
|
|
281
336
|
export function retiredAntigravityFlashTier(modelId: string): string | undefined {
|
|
282
|
-
return RETIRED_FLASH_TIERS[modelId];
|
|
337
|
+
return Object.hasOwn(RETIRED_FLASH_TIERS, modelId) ? RETIRED_FLASH_TIERS[modelId] : undefined;
|
|
283
338
|
}
|
|
284
339
|
|
|
285
340
|
/**
|
|
@@ -299,7 +354,7 @@ export function resolveAntigravityEffortWireModel(
|
|
|
299
354
|
// Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
|
|
300
355
|
// current generation and carry the tier the retired id encoded. This runs BEFORE the
|
|
301
356
|
// suffix check because those ids are aliases, and rule 1 would drop the tier.
|
|
302
|
-
const retiredTier =
|
|
357
|
+
const retiredTier = retiredAntigravityFlashTier(modelId);
|
|
303
358
|
if (retiredTier) {
|
|
304
359
|
return {
|
|
305
360
|
wireModelId: GEMINI_FLASH_CURRENT,
|
|
@@ -5,73 +5,22 @@ import type { PolicyCandidateEvidence } from "../evaluator";
|
|
|
5
5
|
import { policyCandidateHealthEvidence } from "../health";
|
|
6
6
|
import type { NormalizedRoutingProfile } from "../profile";
|
|
7
7
|
import { quotaEvidenceForCandidate } from "../quota";
|
|
8
|
-
import {
|
|
9
|
-
compatibilitySuiteKey,
|
|
10
|
-
loadCompatibilityCatalogSnapshot,
|
|
11
|
-
type CompatibilityCatalogSnapshot,
|
|
12
|
-
} from "./catalog";
|
|
13
|
-
import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
|
|
14
|
-
import {
|
|
15
|
-
resolvePolicyCompatibilitySubjects,
|
|
16
|
-
type ResolvedPolicyCompatibilitySubjects,
|
|
17
|
-
} from "./subject";
|
|
18
|
-
import type { CandidateCompatibilityEvidence } from "./types";
|
|
8
|
+
import { resolveCompatibilityEvidenceProvider, type CoreEvidenceOptions } from "./provider-slot";
|
|
19
9
|
|
|
20
10
|
export type RoutedProviderResolver = (
|
|
21
11
|
providerName: string,
|
|
22
12
|
provider: OcxProviderConfig,
|
|
23
13
|
) => OcxProviderConfig;
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
resolved: ResolvedPolicyCompatibilitySubjects | undefined,
|
|
35
|
-
snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
|
|
36
|
-
catalog: CompatibilityCatalogSnapshot,
|
|
37
|
-
profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
|
|
38
|
-
): CandidateCompatibilityEvidence {
|
|
39
|
-
const subjectIds = resolved?.subjectIds ?? {};
|
|
40
|
-
const suites: CandidateCompatibilityEvidence["suites"] = [];
|
|
41
|
-
|
|
42
|
-
for (const requirement of profile.requiredSuites) {
|
|
43
|
-
const subjectId = subjectIds[requirement.evidenceLayer];
|
|
44
|
-
if (!subjectId) continue;
|
|
45
|
-
const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
|
|
46
|
-
if (!metadata) continue;
|
|
47
|
-
const row = findVerdictForSuite(
|
|
48
|
-
snapshot,
|
|
49
|
-
subjectId,
|
|
50
|
-
requirement.evidenceLayer,
|
|
51
|
-
requirement.suiteId,
|
|
52
|
-
metadata.suiteVersion,
|
|
53
|
-
metadata.suiteManifestDigest,
|
|
54
|
-
);
|
|
55
|
-
if (!row) continue;
|
|
56
|
-
suites.push({
|
|
57
|
-
subjectId,
|
|
58
|
-
suiteId: row.suiteId,
|
|
59
|
-
evidenceLayer: requirement.evidenceLayer,
|
|
60
|
-
suiteVersion: row.suiteVersion,
|
|
61
|
-
suiteManifestDigest: row.suiteManifestDigest,
|
|
62
|
-
verdict: row.verdict,
|
|
63
|
-
asOf: row.asOf,
|
|
64
|
-
maxAgeMs: metadata.maxAgeMs,
|
|
65
|
-
notes: row.notes,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
subjectIds: { ...subjectIds },
|
|
71
|
-
projectionAvailable: snapshot.projectionAvailable,
|
|
72
|
-
suites,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
15
|
+
/**
|
|
16
|
+
* Options the core assembler needs. Provider-specific test seams (subject resolution,
|
|
17
|
+
* catalog and projection loading) belong to the compatibility provider, not here -- keeping
|
|
18
|
+
* them out is what stops the core assembler from naming Lab-backed contracts.
|
|
19
|
+
*
|
|
20
|
+
* The provider reads its own seams off the same object, so callers may still pass a
|
|
21
|
+
* `LabCompatibilityProviderOptions`; that type extends this one.
|
|
22
|
+
*/
|
|
23
|
+
export type AssemblePolicyEvidenceOptions = CoreEvidenceOptions;
|
|
75
24
|
|
|
76
25
|
/**
|
|
77
26
|
* Assemble production policy candidate evidence including compatibility snapshots.
|
|
@@ -89,55 +38,20 @@ export function assemblePolicyCandidateEvidence(
|
|
|
89
38
|
const hasCompatibilityRequirements = Boolean(
|
|
90
39
|
compatibilityPolicy && compatibilityPolicy.requiredSuites.length > 0,
|
|
91
40
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const loadCatalog = options.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
|
|
103
|
-
const loadEvidence = options.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
|
|
104
|
-
catalog = loadCatalog(compatibilityPolicy.requiredSuites);
|
|
105
|
-
const subjectIds = new Set<string>();
|
|
106
|
-
|
|
107
|
-
for (const candidate of profile.candidates) {
|
|
108
|
-
const provider = config.providers[candidate.provider];
|
|
109
|
-
if (!provider) continue;
|
|
110
|
-
try {
|
|
111
|
-
const routed = options.routedProviderConfig(candidate.provider, provider);
|
|
112
|
-
const resolved = resolveSubjects(
|
|
113
|
-
config,
|
|
114
|
-
candidate.provider,
|
|
115
|
-
candidate.model,
|
|
116
|
-
routed,
|
|
117
|
-
options.configDir,
|
|
118
|
-
);
|
|
119
|
-
resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
|
|
120
|
-
for (const subjectId of Object.values(resolved.subjectIds)) {
|
|
121
|
-
if (subjectId) subjectIds.add(subjectId);
|
|
122
|
-
}
|
|
123
|
-
} catch {
|
|
124
|
-
// Subject construction failure is handled per required layer as unknown.
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
snapshot = loadEvidence([...subjectIds], options.configDir);
|
|
129
|
-
}
|
|
41
|
+
// Compatibility evidence is supplied by an opt-in subsystem. With no provider registered
|
|
42
|
+
// -- every install without compatibility-gated profiles -- the evaluator sees no
|
|
43
|
+
// compatibility evidence and scores on capability, health, quota, and cost exactly as it
|
|
44
|
+
// did before compatibility policy existed.
|
|
45
|
+
const compatibilityProvider = hasCompatibilityRequirements && compatibilityPolicy
|
|
46
|
+
? resolveCompatibilityEvidenceProvider()
|
|
47
|
+
: null;
|
|
48
|
+
const compatibilityByCandidate = compatibilityProvider && compatibilityPolicy
|
|
49
|
+
? compatibilityProvider(config, profile, compatibilityPolicy, options)
|
|
50
|
+
: null;
|
|
130
51
|
|
|
131
52
|
return profile.candidates.map(candidate => {
|
|
132
53
|
const key = `${candidate.provider}/${candidate.model}`;
|
|
133
|
-
const compatibility =
|
|
134
|
-
? attachCompatibilityEvidence(
|
|
135
|
-
resolvedByCandidate.get(key),
|
|
136
|
-
snapshot,
|
|
137
|
-
catalog,
|
|
138
|
-
compatibilityPolicy,
|
|
139
|
-
)
|
|
140
|
-
: undefined;
|
|
54
|
+
const compatibility = compatibilityByCandidate?.get(key);
|
|
141
55
|
|
|
142
56
|
return {
|
|
143
57
|
provider: candidate.provider,
|
|
@@ -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);
|