@omercnet/paseo-omp 0.3.0 → 0.4.0-next.114.1
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/README.md +19 -5
- package/client/omp-config-surface.tsx +243 -24
- package/client/omp-config-views.ts +24 -0
- package/client/omp-model-picker-state.ts +145 -0
- package/client/omp-model-picker.tsx +282 -0
- package/client/omp-routing-editor.tsx +307 -0
- package/client/support-diagnostics-state.ts +45 -0
- package/index.server.ts +27 -2
- package/package.json +2 -8
- package/paseo-plugin.json +1 -1
- package/server/omp-models.ts +59 -0
- package/server/omp-settings.ts +30 -20
- package/server/operational-failure-diagnostics.ts +76 -0
- package/server/package-version.ts +2 -0
- package/server/protocol-violation-diagnostics.ts +169 -0
- package/server/provider/catalog.ts +39 -10
- package/server/provider/connection.ts +60 -8
- package/server/provider/host-tools.ts +284 -34
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +1011 -107
- package/server/provider/profile-providers.ts +7 -2
- package/server/provider/registration.ts +12 -2
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +45 -11
- package/server/provider/session.ts +200 -58
- package/server/provider/subsessions.ts +311 -73
- package/server/provider/timeline-projector.ts +34 -11
- package/server/support-diagnostics.ts +284 -0
- package/shared/omp-models.ts +49 -0
- package/shared/omp-settings.ts +227 -3
- package/shared/support-diagnostics.ts +32 -0
- package/CHANGELOG.md +0 -113
- package/SUPPORT.md +0 -44
- package/TESTING.md +0 -150
- package/docs/alpha-release-checklist.md +0 -68
- package/docs/configuration.md +0 -126
- package/docs/core-provider-issue-audit.md +0 -109
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +0 -89
- package/tsconfig.json +0 -16
package/index.server.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./server/mcp-browser";
|
|
7
7
|
import { resolveListOmpMemory } from "./server/memory";
|
|
8
8
|
import { resolveListOmpConfig } from "./server/omp-config";
|
|
9
|
+
import { resolveListOmpModels } from "./server/omp-models";
|
|
9
10
|
import {
|
|
10
11
|
resolveInspectOmpPluginConfig,
|
|
11
12
|
resolveListOmpPlugins,
|
|
@@ -13,7 +14,9 @@ import {
|
|
|
13
14
|
resolveMutateOmpPluginConfig,
|
|
14
15
|
} from "./server/omp-plugins";
|
|
15
16
|
import { resolveListOmpSettings, resolveUpdateOmpSettings } from "./server/omp-settings";
|
|
17
|
+
import { OmpOperationalFailureCollector } from "./server/operational-failure-diagnostics";
|
|
16
18
|
import { withOmpStore } from "./server/paths";
|
|
19
|
+
import { OmpProtocolViolationCollector } from "./server/protocol-violation-diagnostics";
|
|
17
20
|
import { withOmpWorkspaceIdentity } from "./server/provider/host-tools";
|
|
18
21
|
import {
|
|
19
22
|
createProfileOmpProvider,
|
|
@@ -24,11 +27,13 @@ import { createOmpProvider } from "./server/provider/registration";
|
|
|
24
27
|
import { resolveGetOmpProviderHealth } from "./server/provider-diagnostics";
|
|
25
28
|
import { resolveListOmpQuotas } from "./server/quota";
|
|
26
29
|
import { resolveListOmpSessions } from "./server/sessions";
|
|
30
|
+
import { resolveGetOmpSupportReport } from "./server/support-diagnostics";
|
|
27
31
|
import { composerPillSettings } from "./shared/composer-pill-settings";
|
|
28
32
|
import { listHubProcesses, tailHubLog } from "./shared/hub";
|
|
29
33
|
import { openOmpMcpAuthorizationInPaseoBrowser } from "./shared/mcp";
|
|
30
34
|
import { listOmpMemory } from "./shared/memory";
|
|
31
35
|
import { listOmpConfig } from "./shared/omp-config";
|
|
36
|
+
import { listOmpModels } from "./shared/omp-models";
|
|
32
37
|
import {
|
|
33
38
|
inspectOmpPluginConfig,
|
|
34
39
|
listOmpPlugins,
|
|
@@ -40,6 +45,7 @@ import { listOmpStores, type OmpStore } from "./shared/omp-store";
|
|
|
40
45
|
import { getOmpProviderHealth } from "./shared/provider-diagnostics";
|
|
41
46
|
import { listOmpQuotas } from "./shared/quota";
|
|
42
47
|
import { listOmpSessions } from "./shared/sessions";
|
|
48
|
+
import { getOmpSupportReport } from "./shared/support-diagnostics";
|
|
43
49
|
|
|
44
50
|
function scoped<T extends { store?: OmpStore }, R>(handler: (input: T) => R) {
|
|
45
51
|
return (input: T): R => withOmpStore(input.store, () => handler(input));
|
|
@@ -48,10 +54,18 @@ function scoped<T extends { store?: OmpStore }, R>(handler: (input: T) => R) {
|
|
|
48
54
|
export default function contribute(server: PluginServerContext) {
|
|
49
55
|
server.registerSettings(composerPillSettings);
|
|
50
56
|
const browserAuthorizationRegistry = new OmpBrowserAuthorizationRegistry();
|
|
57
|
+
const protocolViolations = new OmpProtocolViolationCollector();
|
|
58
|
+
const operationalFailures = new OmpOperationalFailureCollector();
|
|
51
59
|
const profiles = discoverOmpProfilesSync();
|
|
52
60
|
server.handle(listOmpStores, async () => ({ profiles: await discoverOmpProfiles() }));
|
|
53
61
|
for (const profile of profiles) {
|
|
54
|
-
server.registerProvider(
|
|
62
|
+
server.registerProvider(
|
|
63
|
+
createProfileOmpProvider(profile, {
|
|
64
|
+
browserAuthorizationRegistry,
|
|
65
|
+
reportProtocolViolation: protocolViolations.report,
|
|
66
|
+
reportOperationalFailure: operationalFailures.report,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
55
69
|
}
|
|
56
70
|
server.handle(listHubProcesses, resolveListHubProcesses);
|
|
57
71
|
server.handle(tailHubLog, resolveTailHubLog);
|
|
@@ -59,6 +73,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
59
73
|
server.handle(listOmpMemory, scoped(resolveListOmpMemory));
|
|
60
74
|
server.handle(listOmpSessions, scoped(resolveListOmpSessions));
|
|
61
75
|
server.handle(listOmpConfig, scoped(resolveListOmpConfig));
|
|
76
|
+
server.handle(listOmpModels, scoped(resolveListOmpModels));
|
|
62
77
|
server.handle(listOmpPlugins, scoped(resolveListOmpPlugins));
|
|
63
78
|
server.handle(inspectOmpPluginConfig, scoped(resolveInspectOmpPluginConfig));
|
|
64
79
|
server.handle(mutateOmpPlugin, scoped(resolveMutateOmpPlugin));
|
|
@@ -66,6 +81,10 @@ export default function contribute(server: PluginServerContext) {
|
|
|
66
81
|
server.handle(listOmpSettings, scoped(resolveListOmpSettings));
|
|
67
82
|
server.handle(updateOmpSettings, scoped(resolveUpdateOmpSettings));
|
|
68
83
|
server.handle(getOmpProviderHealth, scoped(resolveGetOmpProviderHealth));
|
|
84
|
+
server.handle(
|
|
85
|
+
getOmpSupportReport,
|
|
86
|
+
scoped((input) => resolveGetOmpSupportReport(input, protocolViolations, operationalFailures)),
|
|
87
|
+
);
|
|
69
88
|
server.handle(openOmpMcpAuthorizationInPaseoBrowser, (input) =>
|
|
70
89
|
resolveOpenOmpMcpAuthorizationInPaseoBrowser(input, browserAuthorizationRegistry),
|
|
71
90
|
);
|
|
@@ -73,7 +92,13 @@ export default function contribute(server: PluginServerContext) {
|
|
|
73
92
|
if (request.provider !== "omp-plugin" && !request.provider.startsWith("omp-plugin-")) return;
|
|
74
93
|
return withOmpWorkspaceIdentity(request);
|
|
75
94
|
});
|
|
76
|
-
server.registerProvider(
|
|
95
|
+
server.registerProvider(
|
|
96
|
+
createOmpProvider({
|
|
97
|
+
browserAuthorizationRegistry,
|
|
98
|
+
reportProtocolViolation: protocolViolations.report,
|
|
99
|
+
reportOperationalFailure: operationalFailures.report,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
77
102
|
return () => {
|
|
78
103
|
browserAuthorizationRegistry.clear();
|
|
79
104
|
removeIdentityHook();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-next.114.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,21 +29,15 @@
|
|
|
29
29
|
"coding-agents"
|
|
30
30
|
],
|
|
31
31
|
"files": [
|
|
32
|
-
"package-lock.json",
|
|
33
|
-
"CHANGELOG.md",
|
|
34
32
|
"LICENSE",
|
|
35
33
|
"README.md",
|
|
36
|
-
"SUPPORT.md",
|
|
37
|
-
"TESTING.md",
|
|
38
|
-
"docs",
|
|
39
34
|
"scripts/prepare-dependencies.mjs",
|
|
40
35
|
"index.client.tsx",
|
|
41
36
|
"index.server.ts",
|
|
42
37
|
"client",
|
|
43
38
|
"server",
|
|
44
39
|
"shared",
|
|
45
|
-
"paseo-plugin.json"
|
|
46
|
-
"tsconfig.json"
|
|
40
|
+
"paseo-plugin.json"
|
|
47
41
|
],
|
|
48
42
|
"scripts": {
|
|
49
43
|
"check": "biome check .",
|
package/paseo-plugin.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
|
+
import type { listOmpModels, OmpModelCandidate, OmpModelListResult } from "../shared/omp-models";
|
|
5
|
+
import { currentOmpEnvironment } from "./paths";
|
|
6
|
+
import {
|
|
7
|
+
type OmpModel,
|
|
8
|
+
OmpRpcRuntime,
|
|
9
|
+
type OmpRuntime,
|
|
10
|
+
type OmpRuntimeSession,
|
|
11
|
+
} from "./provider/omp-rpc";
|
|
12
|
+
import { OmpCleanupFailure, OmpPublicDataSerializer, OmpPublicError } from "./provider/security";
|
|
13
|
+
|
|
14
|
+
function mapOmpModel(model: OmpModel, serializer: OmpPublicDataSerializer): OmpModelCandidate {
|
|
15
|
+
const provider = serializer.text(model.provider, 256);
|
|
16
|
+
const id = serializer.text(model.id, 256);
|
|
17
|
+
return {
|
|
18
|
+
selector: `${provider}/${id}`,
|
|
19
|
+
provider,
|
|
20
|
+
id,
|
|
21
|
+
...(model.name !== undefined ? { name: serializer.text(model.name, 256) } : {}),
|
|
22
|
+
reasoning: model.reasoning === true,
|
|
23
|
+
input: (model.input ?? []).map((value) => serializer.text(value, 256)),
|
|
24
|
+
...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}),
|
|
25
|
+
thinkingLevels: (model.thinking?.efforts ?? []).map((value) => serializer.text(value, 32)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function closeModelCatalogSession(session: OmpRuntimeSession): Promise<void> {
|
|
30
|
+
const cleanup = session.close();
|
|
31
|
+
try {
|
|
32
|
+
await cleanup;
|
|
33
|
+
} catch {
|
|
34
|
+
throw new OmpCleanupFailure("OMP model catalog cleanup failed", cleanup);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resolveListOmpModels(
|
|
39
|
+
input: RpcInput<typeof listOmpModels>,
|
|
40
|
+
runtime: OmpRuntime = new OmpRpcRuntime({ environment: currentOmpEnvironment() }),
|
|
41
|
+
): Promise<OmpModelListResult> {
|
|
42
|
+
const cwd = input.cwd ?? homedir();
|
|
43
|
+
if (!isAbsolute(cwd) || cwd.includes("\0")) {
|
|
44
|
+
throw new OmpPublicError("The workspace path is invalid.");
|
|
45
|
+
}
|
|
46
|
+
const session = await runtime.startSession({
|
|
47
|
+
cwd,
|
|
48
|
+
environment: currentOmpEnvironment(),
|
|
49
|
+
noSession: true,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const serializer = new OmpPublicDataSerializer();
|
|
53
|
+
return {
|
|
54
|
+
models: (await session.getAvailableModels()).map((model) => mapOmpModel(model, serializer)),
|
|
55
|
+
};
|
|
56
|
+
} finally {
|
|
57
|
+
await closeModelCatalogSession(session);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/server/omp-settings.ts
CHANGED
|
@@ -5,12 +5,13 @@ import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
|
5
5
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
6
6
|
import { parseDocument, parse as parseYaml } from "yaml";
|
|
7
7
|
import {
|
|
8
|
+
isOmpStructuredSettingPath,
|
|
8
9
|
type listOmpSettings,
|
|
9
10
|
OMP_SETTINGS_CATALOG_VERSION,
|
|
10
|
-
type OmpScalarValue,
|
|
11
11
|
type OmpSetting,
|
|
12
12
|
type OmpSettingType,
|
|
13
13
|
OmpSettingTypeSchema,
|
|
14
|
+
parseOmpStructuredSettingValue,
|
|
14
15
|
type updateOmpSettings,
|
|
15
16
|
} from "../shared/omp-settings";
|
|
16
17
|
import { SerialMutationQueue } from "./mutation-queue";
|
|
@@ -305,14 +306,28 @@ async function loadCatalog(
|
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
function
|
|
309
|
-
if (
|
|
310
|
-
|
|
309
|
+
function serializeSettingValue(setting: OmpSetting, value: unknown): string | null {
|
|
310
|
+
if (isOmpStructuredSettingPath(setting.path)) {
|
|
311
|
+
const expectedType = setting.path === "cycleOrder" ? "array" : "record";
|
|
312
|
+
if (setting.type !== expectedType) return null;
|
|
313
|
+
const parsed = parseOmpStructuredSettingValue(setting.path, value);
|
|
314
|
+
return parsed === undefined ? null : JSON.stringify(parsed);
|
|
315
|
+
}
|
|
316
|
+
if (setting.type === "boolean") return typeof value === "boolean" ? String(value) : null;
|
|
317
|
+
if (setting.type === "number")
|
|
311
318
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
312
|
-
if (type === "string" || type === "enum")
|
|
319
|
+
if (setting.type === "string" || setting.type === "enum")
|
|
320
|
+
return typeof value === "string" ? value : null;
|
|
313
321
|
return null;
|
|
314
322
|
}
|
|
315
323
|
|
|
324
|
+
function isEditableSetting(setting: OmpSetting): boolean {
|
|
325
|
+
if (setting.redacted) return false;
|
|
326
|
+
if (["boolean", "number", "string", "enum"].includes(setting.type)) return true;
|
|
327
|
+
if (!isOmpStructuredSettingPath(setting.path)) return false;
|
|
328
|
+
return setting.type === (setting.path === "cycleOrder" ? "array" : "record");
|
|
329
|
+
}
|
|
330
|
+
|
|
316
331
|
export async function listOmpSettingsWithDependencies(
|
|
317
332
|
input: RpcInput<typeof listOmpSettings>,
|
|
318
333
|
dependencies: OmpSettingsDependencies,
|
|
@@ -373,26 +388,22 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
373
388
|
if (input.cwd) {
|
|
374
389
|
for (const change of input.changes) {
|
|
375
390
|
const setting = byPath.get(change.path);
|
|
376
|
-
if (
|
|
377
|
-
!setting ||
|
|
378
|
-
setting.redacted ||
|
|
379
|
-
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
380
|
-
) {
|
|
391
|
+
if (!setting || !isEditableSetting(setting)) {
|
|
381
392
|
return {
|
|
382
393
|
conflict: false,
|
|
383
394
|
appliedPaths: [],
|
|
384
395
|
failed: {
|
|
385
396
|
path: change.path,
|
|
386
|
-
message: "This setting cannot be edited
|
|
397
|
+
message: "This setting cannot be edited through the focused configuration editor.",
|
|
387
398
|
},
|
|
388
399
|
catalog: current,
|
|
389
400
|
};
|
|
390
401
|
}
|
|
391
|
-
if (change.operation === "set" &&
|
|
402
|
+
if (change.operation === "set" && serializeSettingValue(setting, change.value) === null) {
|
|
392
403
|
return {
|
|
393
404
|
conflict: false,
|
|
394
405
|
appliedPaths: [],
|
|
395
|
-
failed: { path: change.path, message: `
|
|
406
|
+
failed: { path: change.path, message: `Invalid ${change.path} value.` },
|
|
396
407
|
catalog: current,
|
|
397
408
|
};
|
|
398
409
|
}
|
|
@@ -440,15 +451,14 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
440
451
|
const appliedPaths: string[] = [];
|
|
441
452
|
for (const change of input.changes) {
|
|
442
453
|
const setting = byPath.get(change.path);
|
|
443
|
-
if (
|
|
444
|
-
!setting ||
|
|
445
|
-
setting.redacted ||
|
|
446
|
-
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
447
|
-
) {
|
|
454
|
+
if (!setting || !isEditableSetting(setting)) {
|
|
448
455
|
return {
|
|
449
456
|
conflict: false,
|
|
450
457
|
appliedPaths,
|
|
451
|
-
failed: {
|
|
458
|
+
failed: {
|
|
459
|
+
path: change.path,
|
|
460
|
+
message: "This setting cannot be edited through the focused configuration editor.",
|
|
461
|
+
},
|
|
452
462
|
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
453
463
|
};
|
|
454
464
|
}
|
|
@@ -456,7 +466,7 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
456
466
|
if (change.operation === "reset") {
|
|
457
467
|
args = ["reset", change.path];
|
|
458
468
|
} else {
|
|
459
|
-
const value =
|
|
469
|
+
const value = serializeSettingValue(setting, change.value);
|
|
460
470
|
args = value === null ? null : ["set", change.path, "--json", "--", value];
|
|
461
471
|
}
|
|
462
472
|
if (!args) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const OMP_OPERATIONAL_FAILURES = [
|
|
2
|
+
{ category: "session-open", stage: "startup" },
|
|
3
|
+
{ category: "session-open", stage: "catalog" },
|
|
4
|
+
{ category: "replay-recovery", stage: "persisted-replay" },
|
|
5
|
+
{ category: "replay-recovery", stage: "runtime-recovery" },
|
|
6
|
+
{ category: "replay-recovery", stage: "rewind" },
|
|
7
|
+
{ category: "tool-projector", stage: "host-tool-unknown" },
|
|
8
|
+
{ category: "tool-projector", stage: "host-tool-capacity" },
|
|
9
|
+
{ category: "tool-projector", stage: "host-tool-normalization" },
|
|
10
|
+
{ category: "tool-projector", stage: "host-tool-call" },
|
|
11
|
+
{ category: "tool-projector", stage: "host-tool-timeout" },
|
|
12
|
+
{ category: "tool-projector", stage: "host-tool-delivery" },
|
|
13
|
+
{ category: "tool-projector", stage: "timeline-projector" },
|
|
14
|
+
{ category: "tool-projector", stage: "subsession-projector" },
|
|
15
|
+
{ category: "terminal-outcome", stage: "failed" },
|
|
16
|
+
{ category: "terminal-outcome", stage: "unresolved" },
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type OmpOperationalFailure = (typeof OMP_OPERATIONAL_FAILURES)[number];
|
|
20
|
+
export type OmpOperationalFailureCategory = OmpOperationalFailure["category"];
|
|
21
|
+
export type OmpOperationalFailureStage = OmpOperationalFailure["stage"];
|
|
22
|
+
export type OmpOperationalFailureReporter = (failure: OmpOperationalFailure) => void;
|
|
23
|
+
|
|
24
|
+
export type OmpOperationalFailureSummary = OmpOperationalFailure & {
|
|
25
|
+
occurrenceCount: number;
|
|
26
|
+
firstAt: string | null;
|
|
27
|
+
lastAt: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FAILURE_KEYS: Readonly<Record<string, true>> = Object.fromEntries(
|
|
31
|
+
OMP_OPERATIONAL_FAILURES.map(({ category, stage }) => [`${category}:${stage}`, true]),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
function timestamp(now: () => Date): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const value = now();
|
|
37
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fixed category/stage cells make aggregation constant-space for one plugin subprocess lifetime. */
|
|
44
|
+
export class OmpOperationalFailureCollector {
|
|
45
|
+
private readonly summaries = Object.fromEntries(
|
|
46
|
+
OMP_OPERATIONAL_FAILURES.map(({ category, stage }) => [
|
|
47
|
+
`${category}:${stage}`,
|
|
48
|
+
{ category, stage, occurrenceCount: 0, firstAt: null, lastAt: null },
|
|
49
|
+
]),
|
|
50
|
+
) as Record<string, OmpOperationalFailureSummary>;
|
|
51
|
+
|
|
52
|
+
constructor(private readonly now: () => Date = () => new Date()) {}
|
|
53
|
+
|
|
54
|
+
report: OmpOperationalFailureReporter = (failure) => {
|
|
55
|
+
try {
|
|
56
|
+
const key = `${failure.category}:${failure.stage}`;
|
|
57
|
+
if (!FAILURE_KEYS[key]) return;
|
|
58
|
+
const summary = this.summaries[key];
|
|
59
|
+
if (!summary) return;
|
|
60
|
+
summary.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, summary.occurrenceCount + 1);
|
|
61
|
+
const recordedAt = timestamp(this.now);
|
|
62
|
+
if (recordedAt) {
|
|
63
|
+
summary.firstAt ??= recordedAt;
|
|
64
|
+
summary.lastAt = recordedAt;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Diagnostics must never affect provider or session flow.
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
snapshot(): OmpOperationalFailureSummary[] {
|
|
72
|
+
return OMP_OPERATIONAL_FAILURES.map(({ category, stage }) => ({
|
|
73
|
+
...this.summaries[`${category}:${stage}`],
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OMP_PROTOCOL_VIOLATION_CATEGORIES,
|
|
3
|
+
OMP_PROTOCOL_VIOLATION_REASONS,
|
|
4
|
+
type OmpProtocolViolationCategory,
|
|
5
|
+
type OmpProtocolViolationDiagnostic,
|
|
6
|
+
type OmpProtocolViolationReason,
|
|
7
|
+
} from "./provider/omp-rpc";
|
|
8
|
+
|
|
9
|
+
export interface OmpProtocolViolationSummary {
|
|
10
|
+
category: OmpProtocolViolationCategory;
|
|
11
|
+
occurrenceCount: number;
|
|
12
|
+
batchCount: number;
|
|
13
|
+
maxOccurrenceCount: number;
|
|
14
|
+
firstAt: string | null;
|
|
15
|
+
lastAt: string | null;
|
|
16
|
+
reasonCounts: Record<OmpProtocolViolationReason, number>;
|
|
17
|
+
latestReason: OmpProtocolViolationDiagnostic["reason"] | null;
|
|
18
|
+
latestPhase: OmpProtocolViolationDiagnostic["phase"] | null;
|
|
19
|
+
latestEventType: OmpProtocolViolationDiagnostic["eventType"] | null;
|
|
20
|
+
latestFrameType: OmpProtocolViolationDiagnostic["frameType"] | null;
|
|
21
|
+
latestField: OmpProtocolViolationDiagnostic["field"] | null;
|
|
22
|
+
latestExpected: OmpProtocolViolationDiagnostic["expected"] | null;
|
|
23
|
+
latestActualType: OmpProtocolViolationDiagnostic["actualType"] | null;
|
|
24
|
+
maxByteSize: number | null;
|
|
25
|
+
latestLimitBytes: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type MutableSummary = OmpProtocolViolationSummary;
|
|
29
|
+
|
|
30
|
+
const FRAME_TYPES: Record<NonNullable<OmpProtocolViolationDiagnostic["frameType"]>, true> = {
|
|
31
|
+
ready: true,
|
|
32
|
+
response: true,
|
|
33
|
+
rpc_chunk: true,
|
|
34
|
+
rpc_frame_error: true,
|
|
35
|
+
notice: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function boundedCount(value: unknown): number {
|
|
39
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return 0;
|
|
40
|
+
return Math.min(value, Number.MAX_SAFE_INTEGER);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function boundedAdd(left: number, right: number): number {
|
|
44
|
+
return Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeTimestamp(now: () => Date): string | null {
|
|
48
|
+
try {
|
|
49
|
+
const value = now();
|
|
50
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function logProtocolViolation(message: string, diagnostic: OmpProtocolViolationDiagnostic): void {
|
|
57
|
+
const fields = [
|
|
58
|
+
`category: ${diagnostic.category}`,
|
|
59
|
+
`reason: ${diagnostic.reason}`,
|
|
60
|
+
`occurrenceCount: ${diagnostic.occurrenceCount}`,
|
|
61
|
+
`phase: ${diagnostic.phase}`,
|
|
62
|
+
...(diagnostic.eventType ? [`eventType: ${diagnostic.eventType}`] : []),
|
|
63
|
+
...(diagnostic.frameType ? [`frameType: ${diagnostic.frameType}`] : []),
|
|
64
|
+
...(diagnostic.field ? [`field: ${diagnostic.field}`] : []),
|
|
65
|
+
...(diagnostic.expected ? [`expected: ${diagnostic.expected}`] : []),
|
|
66
|
+
...(diagnostic.actualType ? [`actualType: ${diagnostic.actualType}`] : []),
|
|
67
|
+
...(diagnostic.maxByteSize ? [`maxByteSize: ${diagnostic.maxByteSize}`] : []),
|
|
68
|
+
...(diagnostic.limitBytes ? [`limitBytes: ${diagnostic.limitBytes}`] : []),
|
|
69
|
+
];
|
|
70
|
+
console.error(`${message} { ${fields.join(", ")} }`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fixed-category, saturating in-process aggregation. A new instance is created on each reload. */
|
|
74
|
+
export class OmpProtocolViolationCollector {
|
|
75
|
+
private readonly summaries = Object.fromEntries(
|
|
76
|
+
OMP_PROTOCOL_VIOLATION_CATEGORIES.map((category) => [
|
|
77
|
+
category,
|
|
78
|
+
{
|
|
79
|
+
category,
|
|
80
|
+
occurrenceCount: 0,
|
|
81
|
+
batchCount: 0,
|
|
82
|
+
maxOccurrenceCount: 0,
|
|
83
|
+
firstAt: null,
|
|
84
|
+
lastAt: null,
|
|
85
|
+
reasonCounts: Object.fromEntries(
|
|
86
|
+
OMP_PROTOCOL_VIOLATION_REASONS.map((reason) => [reason, 0]),
|
|
87
|
+
),
|
|
88
|
+
latestReason: null,
|
|
89
|
+
latestPhase: null,
|
|
90
|
+
latestEventType: null,
|
|
91
|
+
latestFrameType: null,
|
|
92
|
+
latestField: null,
|
|
93
|
+
latestExpected: null,
|
|
94
|
+
latestActualType: null,
|
|
95
|
+
maxByteSize: null,
|
|
96
|
+
latestLimitBytes: null,
|
|
97
|
+
},
|
|
98
|
+
]),
|
|
99
|
+
) as Record<OmpProtocolViolationCategory, MutableSummary>;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
private readonly now: () => Date = () => new Date(),
|
|
103
|
+
private readonly log: (
|
|
104
|
+
message: string,
|
|
105
|
+
diagnostic: OmpProtocolViolationDiagnostic,
|
|
106
|
+
) => void | PromiseLike<void> = logProtocolViolation,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
report = (diagnostic: OmpProtocolViolationDiagnostic): void => {
|
|
110
|
+
try {
|
|
111
|
+
const summary = this.summaries[diagnostic.category];
|
|
112
|
+
if (!summary) return;
|
|
113
|
+
const occurrenceCount = boundedCount(diagnostic.occurrenceCount);
|
|
114
|
+
const timestamp = safeTimestamp(this.now);
|
|
115
|
+
summary.occurrenceCount = boundedAdd(summary.occurrenceCount, occurrenceCount);
|
|
116
|
+
summary.batchCount = boundedAdd(summary.batchCount, 1);
|
|
117
|
+
summary.maxOccurrenceCount = Math.max(summary.maxOccurrenceCount, occurrenceCount);
|
|
118
|
+
if (timestamp) {
|
|
119
|
+
summary.firstAt ??= timestamp;
|
|
120
|
+
summary.lastAt = timestamp;
|
|
121
|
+
}
|
|
122
|
+
summary.reasonCounts[diagnostic.reason] = boundedAdd(
|
|
123
|
+
summary.reasonCounts[diagnostic.reason],
|
|
124
|
+
occurrenceCount,
|
|
125
|
+
);
|
|
126
|
+
summary.latestReason = diagnostic.reason;
|
|
127
|
+
summary.latestPhase = diagnostic.phase;
|
|
128
|
+
summary.latestEventType = diagnostic.eventType ?? null;
|
|
129
|
+
summary.latestFrameType =
|
|
130
|
+
diagnostic.frameType && FRAME_TYPES[diagnostic.frameType] ? diagnostic.frameType : null;
|
|
131
|
+
summary.latestField = diagnostic.field ?? null;
|
|
132
|
+
summary.latestExpected = diagnostic.expected ?? null;
|
|
133
|
+
summary.latestActualType = diagnostic.actualType ?? null;
|
|
134
|
+
const byteSize = boundedCount(diagnostic.maxByteSize);
|
|
135
|
+
if (byteSize > 0) summary.maxByteSize = Math.max(summary.maxByteSize ?? 0, byteSize);
|
|
136
|
+
const limitBytes = boundedCount(diagnostic.limitBytes);
|
|
137
|
+
summary.latestLimitBytes = limitBytes || null;
|
|
138
|
+
|
|
139
|
+
const safeDiagnostic: OmpProtocolViolationDiagnostic = {
|
|
140
|
+
category: diagnostic.category,
|
|
141
|
+
reason: diagnostic.reason,
|
|
142
|
+
phase: diagnostic.phase,
|
|
143
|
+
occurrenceCount,
|
|
144
|
+
...(summary.latestEventType ? { eventType: summary.latestEventType } : {}),
|
|
145
|
+
...(summary.latestFrameType ? { frameType: summary.latestFrameType } : {}),
|
|
146
|
+
...(summary.latestField ? { field: summary.latestField } : {}),
|
|
147
|
+
...(summary.latestExpected ? { expected: summary.latestExpected } : {}),
|
|
148
|
+
...(summary.latestActualType ? { actualType: summary.latestActualType } : {}),
|
|
149
|
+
...(byteSize > 0 ? { maxByteSize: byteSize } : {}),
|
|
150
|
+
...(limitBytes > 0 ? { limitBytes } : {}),
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const logging = this.log("OMP protocol violation", safeDiagnostic);
|
|
154
|
+
if (logging) void Promise.resolve(logging).catch(() => undefined);
|
|
155
|
+
} catch {
|
|
156
|
+
// Diagnostics must never affect transport or provider flow.
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Diagnostics must never affect transport or provider flow.
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
snapshot(): OmpProtocolViolationSummary[] {
|
|
164
|
+
return OMP_PROTOCOL_VIOLATION_CATEGORIES.map((category) => ({
|
|
165
|
+
...this.summaries[category],
|
|
166
|
+
reasonCounts: { ...this.summaries[category].reasonCounts },
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -48,6 +48,27 @@ const THINKING_OPTIONS: readonly ProviderThinkingOption[] = [
|
|
|
48
48
|
{ id: "xhigh", label: "XHigh", description: "Extra-high reasoning" },
|
|
49
49
|
{ id: "max", label: "Max", description: "Maximum reasoning" },
|
|
50
50
|
];
|
|
51
|
+
export const OMP_MAX_CATALOG_MODELS = 256;
|
|
52
|
+
|
|
53
|
+
export function selectOmpModels(
|
|
54
|
+
models: readonly OmpModel[],
|
|
55
|
+
activeModel: OmpModel | null | undefined,
|
|
56
|
+
): OmpModel[] {
|
|
57
|
+
if (models.length <= OMP_MAX_CATALOG_MODELS) return [...models];
|
|
58
|
+
const selected = models.slice(0, OMP_MAX_CATALOG_MODELS);
|
|
59
|
+
if (!activeModel) return selected;
|
|
60
|
+
const active = models.find(
|
|
61
|
+
(model) => model.provider === activeModel.provider && model.id === activeModel.id,
|
|
62
|
+
);
|
|
63
|
+
if (
|
|
64
|
+
!active ||
|
|
65
|
+
selected.some((model) => model.provider === active.provider && model.id === active.id)
|
|
66
|
+
) {
|
|
67
|
+
return selected;
|
|
68
|
+
}
|
|
69
|
+
selected[OMP_MAX_CATALOG_MODELS - 1] = active;
|
|
70
|
+
return selected;
|
|
71
|
+
}
|
|
51
72
|
|
|
52
73
|
export function nativeOmpModelId(model: OmpModel): string {
|
|
53
74
|
if (model.provider.includes("/")) {
|
|
@@ -60,14 +81,9 @@ export function ompModelId(model: OmpModel): string {
|
|
|
60
81
|
const nativeIdentity = `${Buffer.byteLength(model.provider, "utf8")}:${model.provider}${Buffer.byteLength(model.id, "utf8")}:${model.id}`;
|
|
61
82
|
return `omp:model:${createHash("sha256").update(nativeIdentity).digest("hex")}`;
|
|
62
83
|
}
|
|
63
|
-
|
|
64
|
-
export function mapOmpModels(
|
|
65
|
-
models: readonly OmpModel[],
|
|
66
|
-
serializer = new OmpPublicDataSerializer(),
|
|
67
|
-
): ProviderModel[] {
|
|
84
|
+
export function validateOmpModelIdentities(models: readonly OmpModel[]): void {
|
|
68
85
|
const seenIds = new Map<string, string>();
|
|
69
|
-
|
|
70
|
-
const thinkingOptions = thinkingForModel(model);
|
|
86
|
+
for (const model of models) {
|
|
71
87
|
const id = ompModelId(model);
|
|
72
88
|
const nativeIdentity = nativeOmpModelId(model);
|
|
73
89
|
const existing = seenIds.get(id);
|
|
@@ -76,6 +92,17 @@ export function mapOmpModels(
|
|
|
76
92
|
}
|
|
77
93
|
if (existing !== undefined) throw new Error("OMP reported a duplicate model identity");
|
|
78
94
|
seenIds.set(id, nativeIdentity);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function mapOmpModels(
|
|
99
|
+
models: readonly OmpModel[],
|
|
100
|
+
serializer = new OmpPublicDataSerializer(),
|
|
101
|
+
): ProviderModel[] {
|
|
102
|
+
validateOmpModelIdentities(models);
|
|
103
|
+
return models.map((model) => {
|
|
104
|
+
const thinkingOptions = thinkingForModel(model);
|
|
105
|
+
const id = ompModelId(model);
|
|
79
106
|
const provider = serializer.text(model.provider, 256);
|
|
80
107
|
const modelId = serializer.text(model.id, 256);
|
|
81
108
|
const name = model.name ? serializer.text(model.name, 256) : modelId;
|
|
@@ -143,14 +170,16 @@ export async function discoverOmpCatalog(
|
|
|
143
170
|
? [...configuredValues, ...(session.inheritedRedactionValues ?? [])]
|
|
144
171
|
: configuredValues,
|
|
145
172
|
);
|
|
146
|
-
|
|
173
|
+
validateOmpModelIdentities(nativeModels);
|
|
174
|
+
const selectedNativeModels = selectOmpModels(nativeModels, state.model);
|
|
175
|
+
const models = mapOmpModels(selectedNativeModels, serializer);
|
|
147
176
|
if (models.length === 0) throw new Error("OMP reported no available models");
|
|
148
177
|
const defaultModel = state.model ? ompModelId(state.model) : models[0]?.id;
|
|
149
178
|
const currentModel = state.model
|
|
150
|
-
?
|
|
179
|
+
? selectedNativeModels.find(
|
|
151
180
|
(model) => model.provider === state.model?.provider && model.id === state.model.id,
|
|
152
181
|
)
|
|
153
|
-
:
|
|
182
|
+
: selectedNativeModels[0];
|
|
154
183
|
if (state.model && !currentModel) throw new Error("OMP reported an unadvertised active model");
|
|
155
184
|
const thinkingOptions = thinkingForModel(currentModel);
|
|
156
185
|
const defaultThinkingOption = thinkingOptions.some(
|