@openpond/evals 0.4.2 → 0.6.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/CONTRACT.md +27 -1
- package/README.md +31 -2
- package/conformance/telemetry/v1/invalid-batch.json +17 -0
- package/conformance/telemetry/v1/valid-batch.json +58 -0
- package/dist/artifact-verification.js +172 -0
- package/dist/builtin-benchmarks/harness-refiner.js +469 -24
- package/dist/execution-contracts.js +153 -0
- package/dist/execution-receipts.js +185 -0
- package/dist/graders.js +8 -2
- package/dist/index.js +10 -0
- package/dist/learned-preference.js +334 -0
- package/dist/preferences.js +803 -0
- package/dist/rollouts.js +241 -0
- package/dist/tasksets.js +24 -0
- package/dist/telemetry/index.js +4 -0
- package/dist/telemetry-analysis.js +183 -0
- package/dist/telemetry-bundle.js +60 -0
- package/dist/telemetry-catalog.js +50 -0
- package/dist/telemetry.js +112 -0
- package/dist/types/artifact-verification.d.ts +31 -0
- package/dist/types/artifact-verification.d.ts.map +1 -0
- package/dist/types/builtin-benchmarks/harness-refiner.d.ts +22 -0
- package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
- package/dist/types/conformance.d.ts +44 -0
- package/dist/types/conformance.d.ts.map +1 -1
- package/dist/types/evidence/conformance.d.ts +24 -24
- package/dist/types/evidence/contracts.d.ts +42 -42
- package/dist/types/execution-contracts.d.ts +806 -0
- package/dist/types/execution-contracts.d.ts.map +1 -0
- package/dist/types/execution-receipts.d.ts +55 -0
- package/dist/types/execution-receipts.d.ts.map +1 -0
- package/dist/types/graders.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/learned-preference.d.ts +282 -0
- package/dist/types/learned-preference.d.ts.map +1 -0
- package/dist/types/preferences.d.ts +583 -0
- package/dist/types/preferences.d.ts.map +1 -0
- package/dist/types/review-conformance.d.ts +20 -15
- package/dist/types/review-conformance.d.ts.map +1 -1
- package/dist/types/rollouts.d.ts +221 -0
- package/dist/types/rollouts.d.ts.map +1 -0
- package/dist/types/tasksets.d.ts +89 -0
- package/dist/types/tasksets.d.ts.map +1 -1
- package/dist/types/telemetry/index.d.ts +5 -0
- package/dist/types/telemetry/index.d.ts.map +1 -0
- package/dist/types/telemetry-analysis.d.ts +116 -0
- package/dist/types/telemetry-analysis.d.ts.map +1 -0
- package/dist/types/telemetry-bundle.d.ts +309 -0
- package/dist/types/telemetry-bundle.d.ts.map +1 -0
- package/dist/types/telemetry-catalog.d.ts +269 -0
- package/dist/types/telemetry-catalog.d.ts.map +1 -0
- package/dist/types/telemetry.d.ts +266 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/package.json +20 -2
- package/schemas/telemetry/v1/evidence-completeness.schema.json +82 -0
- package/schemas/telemetry/v1/evidence-reference.schema.json +41 -0
- package/schemas/telemetry/v1/metric-definition.schema.json +96 -0
- package/schemas/telemetry/v1/metric-observation.schema.json +182 -0
- package/schemas/telemetry/v1/run-metric-summary.schema.json +98 -0
- package/schemas/telemetry/v1/run-telemetry-batch.schema.json +405 -0
- package/schemas/telemetry/v1/run-telemetry-event.schema.json +201 -0
- package/schemas/telemetry/v1/telemetry-cohort.schema.json +100 -0
- package/schemas/telemetry/v1/telemetry-export-bundle.schema.json +655 -0
package/dist/rollouts.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ImmutableArtifactRefSchema, ImmutableReleaseRefSchema, MetadataSchema, ModelRefSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash, } from "@openpond/harness";
|
|
3
|
+
import { AttemptOutcomeClassSchema, FailureOwnerSchema, RewardReceiptSchema, ScoringStatusSchema, } from "./execution-contracts.js";
|
|
4
|
+
import { TaskSplitSchema } from "./tasksets.js";
|
|
5
|
+
import { AttemptReceiptSchema, verifyAttemptReceipt } from "./runs.js";
|
|
6
|
+
export const OptimizerTrainingSampleSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
schemaVersion: z.literal("openpond.optimizerTrainingSample.v1"),
|
|
9
|
+
tokenIds: z.array(z.number().int().nonnegative()).min(2).max(32_768),
|
|
10
|
+
mask: z.array(z.boolean()).min(2).max(32_768),
|
|
11
|
+
logprobs: z.array(z.number().finite()).min(2).max(32_768),
|
|
12
|
+
temperatures: z.array(z.number().positive().finite()).min(2).max(32_768),
|
|
13
|
+
envName: z.string().trim().min(1).max(200),
|
|
14
|
+
modelRequestId: z.string().trim().min(1).max(1_000),
|
|
15
|
+
promptTokenCount: z.number().int().positive(),
|
|
16
|
+
completionTokenCount: z.number().int().positive(),
|
|
17
|
+
servedPolicyVersion: z.number().int().nonnegative(),
|
|
18
|
+
})
|
|
19
|
+
.strict()
|
|
20
|
+
.superRefine((sample, context) => {
|
|
21
|
+
const length = sample.tokenIds.length;
|
|
22
|
+
for (const [name, values] of [
|
|
23
|
+
["mask", sample.mask],
|
|
24
|
+
["logprobs", sample.logprobs],
|
|
25
|
+
["temperatures", sample.temperatures],
|
|
26
|
+
]) {
|
|
27
|
+
if (values.length !== length) {
|
|
28
|
+
context.addIssue({
|
|
29
|
+
code: "custom",
|
|
30
|
+
path: [name],
|
|
31
|
+
message: `${name} must align with tokenIds`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (sample.promptTokenCount + sample.completionTokenCount !== length) {
|
|
36
|
+
context.addIssue({
|
|
37
|
+
code: "custom",
|
|
38
|
+
path: ["completionTokenCount"],
|
|
39
|
+
message: "prompt and completion token counts must span tokenIds",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (sample.mask.filter((trainable) => !trainable).length !== sample.promptTokenCount) {
|
|
43
|
+
context.addIssue({
|
|
44
|
+
code: "custom",
|
|
45
|
+
path: ["mask"],
|
|
46
|
+
message: "promptTokenCount must equal the non-trainable mask count",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (sample.mask.filter(Boolean).length !== sample.completionTokenCount) {
|
|
50
|
+
context.addIssue({
|
|
51
|
+
code: "custom",
|
|
52
|
+
path: ["mask"],
|
|
53
|
+
message: "completionTokenCount must equal the trainable mask count",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
export const EnvironmentExecutionEvidenceSchema = z.object({
|
|
58
|
+
id: ReleaseIdSchema,
|
|
59
|
+
environmentRelease: ImmutableReleaseRefSchema,
|
|
60
|
+
status: z.enum(["completed", "failed", "timed_out", "cancelled"]),
|
|
61
|
+
startedAt: ReleaseTimestampSchema,
|
|
62
|
+
completedAt: ReleaseTimestampSchema,
|
|
63
|
+
traceRefs: z.array(ImmutableArtifactRefSchema).max(10_000),
|
|
64
|
+
metadata: MetadataSchema,
|
|
65
|
+
}).strict();
|
|
66
|
+
const RolloutRewardProjectionSchema = z.object({
|
|
67
|
+
receiptRef: ImmutableReleaseRefSchema,
|
|
68
|
+
status: ScoringStatusSchema,
|
|
69
|
+
value: z.number().min(0).max(1).nullable(),
|
|
70
|
+
learningEligible: z.boolean(),
|
|
71
|
+
passed: z.boolean(),
|
|
72
|
+
outcomeClass: AttemptOutcomeClassSchema,
|
|
73
|
+
failureOwner: FailureOwnerSchema.nullable(),
|
|
74
|
+
components: z.record(ReleaseIdSchema, z.number().min(0).max(1).nullable()),
|
|
75
|
+
}).strict();
|
|
76
|
+
const CanonicalRolloutRecordFieldsSchema = z.object({
|
|
77
|
+
schemaVersion: z.literal("openpond.canonicalRolloutRecord.v1"),
|
|
78
|
+
id: ReleaseIdSchema,
|
|
79
|
+
attemptRef: ImmutableReleaseRefSchema,
|
|
80
|
+
artifactManifestRef: ImmutableReleaseRefSchema,
|
|
81
|
+
tasksetRelease: ImmutableReleaseRefSchema,
|
|
82
|
+
environmentRelease: ImmutableReleaseRefSchema,
|
|
83
|
+
verifierSetRelease: ImmutableReleaseRefSchema,
|
|
84
|
+
harnessRelease: ImmutableReleaseRefSchema,
|
|
85
|
+
taskId: ReleaseIdSchema,
|
|
86
|
+
split: TaskSplitSchema,
|
|
87
|
+
model: ModelRefSchema,
|
|
88
|
+
seed: z.string().trim().min(1).max(500),
|
|
89
|
+
reward: RolloutRewardProjectionSchema,
|
|
90
|
+
traceRef: ImmutableArtifactRefSchema,
|
|
91
|
+
optimizerSample: OptimizerTrainingSampleSchema.nullable(),
|
|
92
|
+
environmentExecutions: z.array(EnvironmentExecutionEvidenceSchema).min(1).max(100_000),
|
|
93
|
+
startedAt: ReleaseTimestampSchema,
|
|
94
|
+
completedAt: ReleaseTimestampSchema,
|
|
95
|
+
metadata: MetadataSchema,
|
|
96
|
+
}).strict();
|
|
97
|
+
function validateCanonicalRolloutRecord(record, context) {
|
|
98
|
+
if (record.reward.status === "scored" && (record.reward.value === null || !record.reward.learningEligible)) {
|
|
99
|
+
context.addIssue({
|
|
100
|
+
code: "custom",
|
|
101
|
+
path: ["reward", "status"],
|
|
102
|
+
message: "A scored rollout requires a numeric, learning-eligible reward.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (record.reward.status === "unscorable" && (record.reward.value !== null || record.reward.learningEligible)) {
|
|
106
|
+
context.addIssue({
|
|
107
|
+
code: "custom",
|
|
108
|
+
path: ["reward", "status"],
|
|
109
|
+
message: "An unscorable rollout has no reward and cannot be learning-eligible.",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const CanonicalRolloutRecordBaseSchema = CanonicalRolloutRecordFieldsSchema
|
|
114
|
+
.superRefine(validateCanonicalRolloutRecord);
|
|
115
|
+
export const CanonicalRolloutRecordSchema = z.object({
|
|
116
|
+
...CanonicalRolloutRecordFieldsSchema.shape,
|
|
117
|
+
contentHash: ReleaseHashSchema,
|
|
118
|
+
}).strict().superRefine(validateCanonicalRolloutRecord);
|
|
119
|
+
export const RolloutQualificationSchema = z.object({
|
|
120
|
+
schemaVersion: z.literal("openpond.rolloutQualification.v1"),
|
|
121
|
+
rolloutCount: z.number().int().nonnegative(),
|
|
122
|
+
scoredCount: z.number().int().nonnegative(),
|
|
123
|
+
optimizerEligibleCount: z.number().int().nonnegative(),
|
|
124
|
+
unscorableCount: z.number().int().nonnegative(),
|
|
125
|
+
zeroRewardCount: z.number().int().nonnegative(),
|
|
126
|
+
rewardMean: z.number().min(0).max(1).nullable(),
|
|
127
|
+
rewardVariance: z.number().nonnegative().nullable(),
|
|
128
|
+
distinctRewardCount: z.number().int().nonnegative(),
|
|
129
|
+
eligibleForRl: z.boolean(),
|
|
130
|
+
reasons: z.array(z.string().trim().min(1).max(1_000)).max(100),
|
|
131
|
+
}).strict();
|
|
132
|
+
export function createCanonicalRolloutRecord(input) {
|
|
133
|
+
const attemptReceipt = AttemptReceiptSchema.parse(input.attemptReceipt);
|
|
134
|
+
if (!verifyAttemptReceipt(attemptReceipt)) {
|
|
135
|
+
throw new Error("Rollout Attempt Receipt failed content-hash verification.");
|
|
136
|
+
}
|
|
137
|
+
const rewardReceipt = RewardReceiptSchema.parse(input.rewardReceipt);
|
|
138
|
+
if (rewardReceipt.attemptRef.id !== attemptReceipt.id
|
|
139
|
+
|| rewardReceipt.attemptRef.contentHash !== attemptReceipt.contentHash) {
|
|
140
|
+
throw new Error("Rollout Attempt Receipt does not match its Reward Receipt.");
|
|
141
|
+
}
|
|
142
|
+
if (rewardReceipt.artifactManifestRef.id !== input.artifactManifestRef.id
|
|
143
|
+
|| rewardReceipt.artifactManifestRef.contentHash !== input.artifactManifestRef.contentHash) {
|
|
144
|
+
throw new Error("Rollout Artifact Manifest does not match its Reward Receipt.");
|
|
145
|
+
}
|
|
146
|
+
if (input.environmentExecutions.some((execution) => execution.environmentRelease.id !== input.environmentRelease.id
|
|
147
|
+
|| execution.environmentRelease.contentHash !== input.environmentRelease.contentHash)) {
|
|
148
|
+
throw new Error("Rollout Environment execution does not match its admitted Environment Release.");
|
|
149
|
+
}
|
|
150
|
+
const content = CanonicalRolloutRecordBaseSchema.parse({
|
|
151
|
+
schemaVersion: "openpond.canonicalRolloutRecord.v1",
|
|
152
|
+
id: input.id,
|
|
153
|
+
attemptRef: rewardReceipt.attemptRef,
|
|
154
|
+
artifactManifestRef: input.artifactManifestRef,
|
|
155
|
+
tasksetRelease: input.tasksetRelease,
|
|
156
|
+
environmentRelease: input.environmentRelease,
|
|
157
|
+
verifierSetRelease: rewardReceipt.verifierSetRef,
|
|
158
|
+
harnessRelease: input.harnessRelease,
|
|
159
|
+
taskId: input.taskId,
|
|
160
|
+
split: input.split,
|
|
161
|
+
model: input.model,
|
|
162
|
+
seed: input.seed,
|
|
163
|
+
reward: {
|
|
164
|
+
receiptRef: { id: rewardReceipt.id, contentHash: rewardReceipt.contentHash },
|
|
165
|
+
status: rewardReceipt.status,
|
|
166
|
+
value: rewardReceipt.reward,
|
|
167
|
+
learningEligible: rewardReceipt.learningEligible,
|
|
168
|
+
passed: rewardReceipt.passed,
|
|
169
|
+
outcomeClass: rewardReceipt.outcomeClass,
|
|
170
|
+
failureOwner: rewardReceipt.failureOwner,
|
|
171
|
+
components: Object.fromEntries(rewardReceipt.components.map((component) => [
|
|
172
|
+
component.verifierId,
|
|
173
|
+
component.rewardContribution,
|
|
174
|
+
])),
|
|
175
|
+
},
|
|
176
|
+
traceRef: input.traceRef,
|
|
177
|
+
optimizerSample: input.optimizerSample,
|
|
178
|
+
environmentExecutions: input.environmentExecutions,
|
|
179
|
+
startedAt: input.startedAt,
|
|
180
|
+
completedAt: input.completedAt,
|
|
181
|
+
metadata: input.metadata ?? {},
|
|
182
|
+
});
|
|
183
|
+
return CanonicalRolloutRecordSchema.parse({
|
|
184
|
+
...content,
|
|
185
|
+
contentHash: contentHash(content),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export function verifyCanonicalRolloutRecord(value) {
|
|
189
|
+
const parsed = CanonicalRolloutRecordSchema.safeParse(value);
|
|
190
|
+
if (!parsed.success)
|
|
191
|
+
return false;
|
|
192
|
+
const { contentHash: actual, ...content } = parsed.data;
|
|
193
|
+
const canonical = CanonicalRolloutRecordBaseSchema.safeParse(content);
|
|
194
|
+
return canonical.success && contentHash(canonical.data) === actual;
|
|
195
|
+
}
|
|
196
|
+
export function optimizerEligibleRollouts(records) {
|
|
197
|
+
return records.filter((record) => record.reward.status === "scored"
|
|
198
|
+
&& record.reward.learningEligible
|
|
199
|
+
&& record.reward.value !== null
|
|
200
|
+
&& record.optimizerSample !== null);
|
|
201
|
+
}
|
|
202
|
+
export function qualifyRolloutBatch(input) {
|
|
203
|
+
const records = input.records.map((record) => CanonicalRolloutRecordSchema.parse(record));
|
|
204
|
+
const scored = records.filter((record) => record.reward.status === "scored"
|
|
205
|
+
&& record.reward.learningEligible
|
|
206
|
+
&& record.reward.value !== null);
|
|
207
|
+
const optimizerEligible = optimizerEligibleRollouts(records);
|
|
208
|
+
const rewards = scored.map((record) => record.reward.value);
|
|
209
|
+
const mean = rewards.length
|
|
210
|
+
? rewards.reduce((total, reward) => total + reward, 0) / rewards.length
|
|
211
|
+
: null;
|
|
212
|
+
const variance = mean === null
|
|
213
|
+
? null
|
|
214
|
+
: rewards.reduce((total, reward) => total + (reward - mean) ** 2, 0) / rewards.length;
|
|
215
|
+
const distinctRewardCount = new Set(rewards.map((reward) => reward.toPrecision(12))).size;
|
|
216
|
+
const minimumScoredRollouts = input.minimumScoredRollouts ?? 2;
|
|
217
|
+
const minimumDistinctRewards = input.minimumDistinctRewards ?? 2;
|
|
218
|
+
const minimumRewardVariance = input.minimumRewardVariance ?? Number.EPSILON;
|
|
219
|
+
const reasons = [];
|
|
220
|
+
if (scored.length < minimumScoredRollouts)
|
|
221
|
+
reasons.push(`Requires at least ${minimumScoredRollouts} scored rollouts.`);
|
|
222
|
+
if (optimizerEligible.length !== scored.length)
|
|
223
|
+
reasons.push("Every scored rollout requires aligned optimizer token evidence.");
|
|
224
|
+
if (distinctRewardCount < minimumDistinctRewards)
|
|
225
|
+
reasons.push(`Requires at least ${minimumDistinctRewards} distinct reward values.`);
|
|
226
|
+
if (variance === null || variance < minimumRewardVariance)
|
|
227
|
+
reasons.push(`Reward variance must be at least ${minimumRewardVariance}.`);
|
|
228
|
+
return RolloutQualificationSchema.parse({
|
|
229
|
+
schemaVersion: "openpond.rolloutQualification.v1",
|
|
230
|
+
rolloutCount: records.length,
|
|
231
|
+
scoredCount: scored.length,
|
|
232
|
+
optimizerEligibleCount: optimizerEligible.length,
|
|
233
|
+
unscorableCount: records.length - scored.length,
|
|
234
|
+
zeroRewardCount: rewards.filter((reward) => reward === 0).length,
|
|
235
|
+
rewardMean: mean,
|
|
236
|
+
rewardVariance: variance,
|
|
237
|
+
distinctRewardCount,
|
|
238
|
+
eligibleForRl: reasons.length === 0,
|
|
239
|
+
reasons,
|
|
240
|
+
});
|
|
241
|
+
}
|
package/dist/tasksets.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CapabilityRequirementSchema, ImmutableAssetRefSchema, MetadataSchema, ReleaseHashSchema, ReleaseIdSchema, ToolDeclarationSchema, assertContentHash, contentHash, } from "@openpond/harness";
|
|
3
3
|
export const TaskSplitSchema = z.enum(["train", "validation", "test", "frozen_eval"]);
|
|
4
|
+
export const RequiredOutputContractSchema = z.object({
|
|
5
|
+
path: z.string().trim().min(1).max(2_000).refine(safeRelativePath),
|
|
6
|
+
mediaType: z.string().trim().min(1).max(200),
|
|
7
|
+
schemaRef: ImmutableAssetRefSchema.nullable().default(null),
|
|
8
|
+
maxBytes: z.number().int().positive().max(250_000_000).nullable().default(null),
|
|
9
|
+
metadata: MetadataSchema,
|
|
10
|
+
}).strict();
|
|
4
11
|
export const PolicyBoundarySchema = z.object({
|
|
5
12
|
policyVisibleFields: z.array(ReleaseIdSchema).max(1_000).default([]),
|
|
6
13
|
privilegedFields: z.array(ReleaseIdSchema).max(1_000).default([]),
|
|
@@ -60,6 +67,7 @@ export const TaskRecordSchema = z.object({
|
|
|
60
67
|
policyVisibleContext: z.record(z.string(), z.unknown()).default({}),
|
|
61
68
|
privilegedContextRef: ReleaseIdSchema.nullable(),
|
|
62
69
|
artifactRefs: z.array(ImmutableAssetRefSchema).max(1_000).default([]),
|
|
70
|
+
requiredOutputs: z.array(RequiredOutputContractSchema).max(1_000).optional(),
|
|
63
71
|
tags: z.array(ReleaseIdSchema).max(100).default([]),
|
|
64
72
|
}).strict();
|
|
65
73
|
export const TasksetReleaseContentSchema = z.object({
|
|
@@ -68,10 +76,12 @@ export const TasksetReleaseContentSchema = z.object({
|
|
|
68
76
|
revision: z.number().int().positive(),
|
|
69
77
|
policy: PolicyBoundarySchema,
|
|
70
78
|
environment: EnvironmentContractSchema,
|
|
79
|
+
environmentRelease: z.object({ id: ReleaseIdSchema, contentHash: ReleaseHashSchema }).strict().optional(),
|
|
71
80
|
tools: z.array(ToolDeclarationSchema).max(200),
|
|
72
81
|
capabilities: z.array(CapabilityRequirementSchema).max(200),
|
|
73
82
|
tasks: z.array(TaskRecordSchema).min(1).max(1_000_000),
|
|
74
83
|
graders: z.array(GraderSpecSchema).min(1).max(1_000),
|
|
84
|
+
verifierSetRelease: z.object({ id: ReleaseIdSchema, contentHash: ReleaseHashSchema }).strict().optional(),
|
|
75
85
|
metadata: MetadataSchema,
|
|
76
86
|
}).strict();
|
|
77
87
|
export const TasksetReleaseSchema = TasksetReleaseContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
|
|
@@ -92,6 +102,14 @@ export function validateTasksetRelease(input) {
|
|
|
92
102
|
}
|
|
93
103
|
const taskset = parsed.data;
|
|
94
104
|
const issues = [];
|
|
105
|
+
if (Boolean(taskset.environmentRelease) !== Boolean(taskset.verifierSetRelease)) {
|
|
106
|
+
issues.push({
|
|
107
|
+
code: "execution_release_binding_incomplete",
|
|
108
|
+
severity: "error",
|
|
109
|
+
message: "A Taskset Release must bind both Environment and Verifier Set releases or neither during v2 migration.",
|
|
110
|
+
path: "environmentRelease",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
95
113
|
const clusterSplits = new Map();
|
|
96
114
|
for (const task of taskset.tasks) {
|
|
97
115
|
const splits = clusterSplits.get(task.clusterKey) ?? new Set();
|
|
@@ -147,3 +165,9 @@ export function trainingPolicyTaskViews(taskset) {
|
|
|
147
165
|
.map(policyTaskView);
|
|
148
166
|
}
|
|
149
167
|
export { CapabilityRequirementSchema, ToolDeclarationSchema, } from "@openpond/harness";
|
|
168
|
+
function safeRelativePath(value) {
|
|
169
|
+
const normalized = value.replaceAll("\\", "/");
|
|
170
|
+
if (!normalized || normalized.startsWith("/") || normalized.includes("\0"))
|
|
171
|
+
return false;
|
|
172
|
+
return !normalized.split("/").some((part) => !part || part === "." || part === "..");
|
|
173
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
|
|
3
|
+
import { getCoreMetricDefinition, validateCoreMetricObservation } from "./telemetry-catalog.js";
|
|
4
|
+
import { MetricObservationSchema, RunTelemetryEventSchema, TelemetryVisibilitySchema, } from "./telemetry.js";
|
|
5
|
+
export const TelemetryCohortSchema = z.object({
|
|
6
|
+
checkpointIds: z.array(ReleaseIdSchema).max(1_000),
|
|
7
|
+
steps: z.array(z.number().int().nonnegative()).max(10_000),
|
|
8
|
+
scenarioIds: z.array(ReleaseIdSchema).max(100_000),
|
|
9
|
+
rolloutGroupIds: z.array(ReleaseIdSchema).max(100_000),
|
|
10
|
+
attemptIds: z.array(ReleaseIdSchema).max(1_000_000),
|
|
11
|
+
splits: z.array(z.string().trim().min(1).max(100)).max(32),
|
|
12
|
+
failureOwners: z.array(z.string().trim().min(1).max(100)).max(32),
|
|
13
|
+
graders: z.array(z.string().trim().min(1).max(200)).max(1_000),
|
|
14
|
+
rewardEligible: z.boolean().nullable(),
|
|
15
|
+
}).strict();
|
|
16
|
+
export const EvidenceReferenceSchema = z.object({
|
|
17
|
+
id: ReleaseIdSchema,
|
|
18
|
+
contentHash: ReleaseHashSchema,
|
|
19
|
+
kind: z.enum(["rollout", "attempt", "trace", "grader", "checkpoint", "artifact"]),
|
|
20
|
+
visibility: TelemetryVisibilitySchema,
|
|
21
|
+
}).strict();
|
|
22
|
+
export const EvidenceCompletenessSchema = z.object({
|
|
23
|
+
schemaVersion: z.literal("openpond.telemetryEvidenceCompleteness.v1"),
|
|
24
|
+
runId: ReleaseIdSchema,
|
|
25
|
+
status: z.enum(["complete", "partial", "missing"]),
|
|
26
|
+
expectedEventTypes: z.array(z.string().trim().min(1).max(100)).max(100),
|
|
27
|
+
observedEventTypes: z.array(z.string().trim().min(1).max(100)).max(100),
|
|
28
|
+
missingEventTypes: z.array(z.string().trim().min(1).max(100)).max(100),
|
|
29
|
+
lastSequence: z.number().int().nonnegative().nullable(),
|
|
30
|
+
sequenceGaps: z.array(z.number().int().nonnegative()).max(10_000),
|
|
31
|
+
}).strict();
|
|
32
|
+
export const MetricSeriesPointSchema = z.object({
|
|
33
|
+
step: z.number().int().nonnegative().nullable(),
|
|
34
|
+
observedAt: ReleaseTimestampSchema,
|
|
35
|
+
value: z.number().finite(),
|
|
36
|
+
sampleCount: z.number().int().positive(),
|
|
37
|
+
}).strict();
|
|
38
|
+
export const RunMetricSummarySchema = z.object({
|
|
39
|
+
schemaVersion: z.literal("openpond.runMetricSummary.v1"),
|
|
40
|
+
runId: ReleaseIdSchema,
|
|
41
|
+
metricId: ReleaseIdSchema,
|
|
42
|
+
aggregation: z.enum(["last", "sum", "mean", "min", "max", "p50", "p95"]),
|
|
43
|
+
value: z.number().finite().nullable(),
|
|
44
|
+
sampleCount: z.number().int().nonnegative(),
|
|
45
|
+
series: z.array(MetricSeriesPointSchema).max(100_000),
|
|
46
|
+
}).strict();
|
|
47
|
+
export function createRunTelemetryEvent(input) {
|
|
48
|
+
const idHash = contentHash({ runId: input.lineage.runId, sequence: input.sequence, type: input.type, source: input.source });
|
|
49
|
+
return RunTelemetryEventSchema.parse({
|
|
50
|
+
schemaVersion: "openpond.runTelemetryEvent.v1",
|
|
51
|
+
eventId: `telemetry-${idHash.slice(0, 32)}`,
|
|
52
|
+
...input,
|
|
53
|
+
attributes: input.attributes ?? {},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function createMetricObservation(input) {
|
|
57
|
+
const idHash = contentHash({ runId: input.lineage.runId, metricId: input.metricId, sequence: input.sequence });
|
|
58
|
+
return validateCoreMetricObservation(MetricObservationSchema.parse({
|
|
59
|
+
schemaVersion: "openpond.metricObservation.v1",
|
|
60
|
+
observationId: `metric-${idHash.slice(0, 32)}`,
|
|
61
|
+
...input,
|
|
62
|
+
dimensions: input.dimensions ?? {},
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
export function telemetryIdempotencyKey(item) {
|
|
66
|
+
const id = item.schemaVersion === "openpond.metricObservation.v1" ? item.observationId : item.eventId;
|
|
67
|
+
return `${item.lineage.runId}:${item.sequence}:${id}`;
|
|
68
|
+
}
|
|
69
|
+
export function mergeTelemetryItems(input) {
|
|
70
|
+
const accepted = new Map();
|
|
71
|
+
const sequences = new Map();
|
|
72
|
+
for (const item of [...input.events, ...input.observations]) {
|
|
73
|
+
const key = telemetryIdempotencyKey(item);
|
|
74
|
+
const existing = accepted.get(key);
|
|
75
|
+
if (existing) {
|
|
76
|
+
if (contentHash(existing) !== contentHash(item))
|
|
77
|
+
throw new Error(`Telemetry idempotency conflict: ${key}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const sequenceKey = `${item.lineage.runId}:${item.sequence}`;
|
|
81
|
+
const priorKey = sequences.get(sequenceKey);
|
|
82
|
+
if (priorKey && priorKey !== key)
|
|
83
|
+
throw new Error(`Telemetry sequence conflict: ${sequenceKey}`);
|
|
84
|
+
sequences.set(sequenceKey, key);
|
|
85
|
+
accepted.set(key, item);
|
|
86
|
+
}
|
|
87
|
+
const sorted = [...accepted.values()].sort((left, right) => left.sequence - right.sequence);
|
|
88
|
+
return {
|
|
89
|
+
events: sorted.filter((item) => item.schemaVersion === "openpond.runTelemetryEvent.v1"),
|
|
90
|
+
observations: sorted.filter((item) => item.schemaVersion === "openpond.metricObservation.v1"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function matchesOptionalSet(values, candidate) {
|
|
94
|
+
return values.length === 0 || (candidate !== null && values.includes(candidate));
|
|
95
|
+
}
|
|
96
|
+
export function filterTelemetryCohort(input) {
|
|
97
|
+
const cohort = TelemetryCohortSchema.parse(input.cohort);
|
|
98
|
+
const lineageMatches = (lineage) => matchesOptionalSet(cohort.checkpointIds, lineage.checkpointId)
|
|
99
|
+
&& matchesOptionalSet(cohort.steps, lineage.step)
|
|
100
|
+
&& matchesOptionalSet(cohort.scenarioIds, lineage.scenarioId)
|
|
101
|
+
&& matchesOptionalSet(cohort.rolloutGroupIds, lineage.rolloutGroupId)
|
|
102
|
+
&& matchesOptionalSet(cohort.attemptIds, lineage.attemptId);
|
|
103
|
+
const attributeMatches = (values) => matchesOptionalSet(cohort.splits, typeof values.split === "string" ? values.split : null)
|
|
104
|
+
&& matchesOptionalSet(cohort.failureOwners, typeof values.failureOwner === "string" ? values.failureOwner : null)
|
|
105
|
+
&& matchesOptionalSet(cohort.graders, typeof values.grader === "string" ? values.grader : null)
|
|
106
|
+
&& (cohort.rewardEligible === null || values.rewardEligible === cohort.rewardEligible);
|
|
107
|
+
return {
|
|
108
|
+
events: input.events.filter((event) => lineageMatches(event.lineage) && attributeMatches(event.attributes)),
|
|
109
|
+
observations: input.observations.filter((observation) => lineageMatches(observation.lineage) && attributeMatches(observation.dimensions)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function deriveEvidenceCompleteness(input) {
|
|
113
|
+
const events = input.events.filter((event) => event.lineage.runId === input.runId).sort((left, right) => left.sequence - right.sequence);
|
|
114
|
+
const observed = [...new Set(events.map((event) => event.type))].sort();
|
|
115
|
+
const missing = [...new Set(input.expectedEventTypes)].filter((type) => !observed.includes(type)).sort();
|
|
116
|
+
const sequenceValues = [...new Set(events.map((event) => event.sequence))].sort((left, right) => left - right);
|
|
117
|
+
const lastSequence = events.at(-1)?.sequence ?? null;
|
|
118
|
+
const sequenceGaps = [];
|
|
119
|
+
let expected = 0;
|
|
120
|
+
for (const sequence of sequenceValues) {
|
|
121
|
+
while (expected < sequence && sequenceGaps.length < 10_000)
|
|
122
|
+
sequenceGaps.push(expected++);
|
|
123
|
+
expected = sequence + 1;
|
|
124
|
+
}
|
|
125
|
+
return EvidenceCompletenessSchema.parse({
|
|
126
|
+
schemaVersion: "openpond.telemetryEvidenceCompleteness.v1",
|
|
127
|
+
runId: input.runId,
|
|
128
|
+
status: events.length === 0 ? "missing" : missing.length || sequenceGaps.length ? "partial" : "complete",
|
|
129
|
+
expectedEventTypes: [...new Set(input.expectedEventTypes)].sort(),
|
|
130
|
+
observedEventTypes: observed,
|
|
131
|
+
missingEventTypes: missing,
|
|
132
|
+
lastSequence,
|
|
133
|
+
sequenceGaps,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function aggregate(values, method) {
|
|
137
|
+
if (!values.length)
|
|
138
|
+
return null;
|
|
139
|
+
if (method === "last")
|
|
140
|
+
return values.at(-1) ?? null;
|
|
141
|
+
if (method === "sum")
|
|
142
|
+
return values.reduce((total, value) => total + value, 0);
|
|
143
|
+
if (method === "mean")
|
|
144
|
+
return values.reduce((total, value) => total + value, 0) / values.length;
|
|
145
|
+
if (method === "min")
|
|
146
|
+
return Math.min(...values);
|
|
147
|
+
if (method === "max")
|
|
148
|
+
return Math.max(...values);
|
|
149
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
150
|
+
const percentile = method === "p50" ? 0.5 : 0.95;
|
|
151
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * percentile) - 1)] ?? null;
|
|
152
|
+
}
|
|
153
|
+
export function summarizeMetric(runId, metricId, observations) {
|
|
154
|
+
const definition = getCoreMetricDefinition(metricId);
|
|
155
|
+
if (!definition)
|
|
156
|
+
throw new Error(`Unknown core metric: ${metricId}`);
|
|
157
|
+
const selected = observations.filter((item) => item.lineage.runId === runId && item.metricId === metricId).sort((left, right) => left.sequence - right.sequence);
|
|
158
|
+
const buckets = new Map();
|
|
159
|
+
for (const item of selected) {
|
|
160
|
+
const key = item.lineage.step === null ? `time:${item.observedAt}` : `step:${item.lineage.step}`;
|
|
161
|
+
buckets.set(key, [...(buckets.get(key) ?? []), item]);
|
|
162
|
+
}
|
|
163
|
+
const series = [...buckets.values()].map((items) => ({
|
|
164
|
+
step: items[0]?.lineage.step ?? null,
|
|
165
|
+
observedAt: items[0]?.observedAt,
|
|
166
|
+
value: aggregate(items.map((item) => item.value), definition.aggregation),
|
|
167
|
+
sampleCount: items.length,
|
|
168
|
+
})).filter((point) => point.value !== null && point.observedAt !== undefined);
|
|
169
|
+
return RunMetricSummarySchema.parse({ schemaVersion: "openpond.runMetricSummary.v1", runId, metricId, aggregation: definition.aggregation, value: aggregate(selected.map((item) => item.value), definition.aggregation), sampleCount: selected.length, series });
|
|
170
|
+
}
|
|
171
|
+
export function redactTelemetryEvent(event, maximumVisibility) {
|
|
172
|
+
const rank = { policy_visible: 0, team_visible: 1, host_private: 2 };
|
|
173
|
+
if (rank[event.visibility] > rank[maximumVisibility])
|
|
174
|
+
return null;
|
|
175
|
+
return RunTelemetryEventSchema.parse(event);
|
|
176
|
+
}
|
|
177
|
+
export function redactTelemetryAttributes(event, deniedKeys) {
|
|
178
|
+
const denied = new Set(deniedKeys);
|
|
179
|
+
return RunTelemetryEventSchema.parse({
|
|
180
|
+
...event,
|
|
181
|
+
attributes: Object.fromEntries(Object.entries(event.attributes).filter(([key]) => !denied.has(key))),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
|
|
3
|
+
import { MetricCatalogSchema } from "./telemetry-catalog.js";
|
|
4
|
+
import { deriveEvidenceCompleteness, EvidenceCompletenessSchema, EvidenceReferenceSchema, redactTelemetryAttributes, redactTelemetryEvent } from "./telemetry-analysis.js";
|
|
5
|
+
import { MetricObservationSchema, RunTelemetryEventSchema } from "./telemetry.js";
|
|
6
|
+
export const TelemetryExportBundleContentSchema = z.object({
|
|
7
|
+
schemaVersion: z.literal("openpond.telemetryExportBundle.v1"),
|
|
8
|
+
id: ReleaseIdSchema,
|
|
9
|
+
runId: ReleaseIdSchema,
|
|
10
|
+
exportedAt: ReleaseTimestampSchema,
|
|
11
|
+
definitions: MetricCatalogSchema,
|
|
12
|
+
events: z.array(RunTelemetryEventSchema).max(1_000_000),
|
|
13
|
+
observations: z.array(MetricObservationSchema).max(10_000_000),
|
|
14
|
+
evidenceRefs: z.array(EvidenceReferenceSchema).max(1_000_000),
|
|
15
|
+
completeness: EvidenceCompletenessSchema,
|
|
16
|
+
}).strict();
|
|
17
|
+
export const TelemetryExportBundleSchema = TelemetryExportBundleContentSchema.extend({
|
|
18
|
+
contentHash: ReleaseHashSchema,
|
|
19
|
+
}).strict();
|
|
20
|
+
export function createTelemetryExportBundle(input) {
|
|
21
|
+
const content = TelemetryExportBundleContentSchema.parse(input);
|
|
22
|
+
if (content.completeness.runId !== content.runId) {
|
|
23
|
+
throw new Error("Telemetry export completeness belongs to another Run.");
|
|
24
|
+
}
|
|
25
|
+
if (content.events.some((event) => event.lineage.runId !== content.runId) || content.observations.some((observation) => observation.lineage.runId !== content.runId)) {
|
|
26
|
+
throw new Error("Telemetry export bundle contains evidence from another Run.");
|
|
27
|
+
}
|
|
28
|
+
return TelemetryExportBundleSchema.parse({ ...content, contentHash: contentHash(content) });
|
|
29
|
+
}
|
|
30
|
+
export function verifyTelemetryExportBundle(input) {
|
|
31
|
+
const parsed = TelemetryExportBundleSchema.safeParse(input);
|
|
32
|
+
if (!parsed.success)
|
|
33
|
+
return false;
|
|
34
|
+
const { contentHash: actual, ...content } = parsed.data;
|
|
35
|
+
return contentHash(TelemetryExportBundleContentSchema.parse(content)) === actual;
|
|
36
|
+
}
|
|
37
|
+
export function redactTelemetryExportBundle(input) {
|
|
38
|
+
const bundle = TelemetryExportBundleSchema.parse(input.bundle);
|
|
39
|
+
if (!verifyTelemetryExportBundle(bundle))
|
|
40
|
+
throw new Error("Telemetry export bundle has an invalid content hash.");
|
|
41
|
+
const rank = { policy_visible: 0, team_visible: 1, host_private: 2 };
|
|
42
|
+
const definitions = bundle.definitions.filter((definition) => rank[definition.visibility] <= rank[input.maximumVisibility]);
|
|
43
|
+
const metricIds = new Set(definitions.map((definition) => definition.id));
|
|
44
|
+
const events = bundle.events.flatMap((event) => {
|
|
45
|
+
const visible = redactTelemetryEvent(event, input.maximumVisibility);
|
|
46
|
+
return visible ? [redactTelemetryAttributes(visible, input.deniedAttributeKeys ?? [])] : [];
|
|
47
|
+
});
|
|
48
|
+
const expectedEventTypes = bundle.completeness.expectedEventTypes.filter((type) => events.some((event) => event.type === type));
|
|
49
|
+
return createTelemetryExportBundle({
|
|
50
|
+
schemaVersion: "openpond.telemetryExportBundle.v1",
|
|
51
|
+
id: input.id,
|
|
52
|
+
runId: bundle.runId,
|
|
53
|
+
exportedAt: input.exportedAt,
|
|
54
|
+
definitions,
|
|
55
|
+
events,
|
|
56
|
+
observations: bundle.observations.filter((observation) => metricIds.has(observation.metricId)),
|
|
57
|
+
evidenceRefs: bundle.evidenceRefs.filter((reference) => rank[reference.visibility] <= rank[input.maximumVisibility]),
|
|
58
|
+
completeness: deriveEvidenceCompleteness({ runId: bundle.runId, events, expectedEventTypes: expectedEventTypes }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MetricDefinitionSchema } from "./telemetry.js";
|
|
3
|
+
const definition = (input) => MetricDefinitionSchema.parse({ schemaVersion: "openpond.metricDefinition.v1", ...input });
|
|
4
|
+
export const CORE_METRIC_CATALOG = [
|
|
5
|
+
definition({ id: "reward.mean", displayName: "Mean reward", description: "Mean composed reward for the cohort.", valueType: "gauge", unit: "scalar", direction: "higher", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split", "grader"] }),
|
|
6
|
+
definition({ id: "reward.variance", displayName: "Reward variance", description: "Population variance of composed reward within a rollout group.", valueType: "gauge", unit: "scalar", direction: "neutral", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
7
|
+
definition({ id: "reward.constant_group_rate", displayName: "Constant group rate", description: "Fraction of rollout groups with no reward variation.", valueType: "gauge", unit: "ratio", direction: "lower", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
8
|
+
definition({ id: "attempt.valid_rate", displayName: "Valid attempt rate", description: "Fraction of attempts passing deterministic validity checks.", valueType: "gauge", unit: "ratio", direction: "higher", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split", "failureOwner"] }),
|
|
9
|
+
definition({ id: "attempt.failure_count", displayName: "Attempt failures", description: "Count of failed Attempts.", valueType: "counter", unit: "count", direction: "lower", aggregation: "sum", visibility: "team_visible", boundedDimensions: ["split", "failureOwner", "failureClass"] }),
|
|
10
|
+
definition({ id: "optimizer.loss", displayName: "Optimizer loss", description: "Policy optimizer loss after the step.", valueType: "gauge", unit: "scalar", direction: "neutral", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
11
|
+
definition({ id: "optimizer.learning_rate", displayName: "Learning rate", description: "Optimizer learning rate after the step.", valueType: "gauge", unit: "scalar", direction: "neutral", aggregation: "last", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
12
|
+
definition({ id: "optimizer.kl", displayName: "KL divergence", description: "Sampled KL divergence from the reference policy.", valueType: "gauge", unit: "scalar", direction: "lower", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
13
|
+
definition({ id: "optimizer.entropy", displayName: "Policy entropy", description: "Observed policy entropy for trainable tokens.", valueType: "gauge", unit: "scalar", direction: "neutral", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
14
|
+
definition({ id: "optimizer.gradient_norm", displayName: "Gradient norm", description: "Gradient norm reported by the optimizer.", valueType: "gauge", unit: "scalar", direction: "neutral", aggregation: "max", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
15
|
+
definition({ id: "optimizer.clip_fraction", displayName: "Clip fraction", description: "Fraction of policy updates affected by clipping.", valueType: "gauge", unit: "ratio", direction: "neutral", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
16
|
+
definition({ id: "output.duplicate_rate", displayName: "Duplicate output rate", description: "Fraction of outputs duplicated within the measured cohort.", valueType: "gauge", unit: "ratio", direction: "lower", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
17
|
+
definition({ id: "output.unique_count", displayName: "Unique outputs", description: "Distinct output count in the measured cohort.", valueType: "gauge", unit: "count", direction: "higher", aggregation: "last", visibility: "team_visible", boundedDimensions: ["split"] }),
|
|
18
|
+
definition({ id: "tokens.input", displayName: "Input tokens", description: "Input tokens processed.", valueType: "counter", unit: "tokens", direction: "neutral", aggregation: "sum", visibility: "team_visible", boundedDimensions: ["split", "source"] }),
|
|
19
|
+
definition({ id: "tokens.output", displayName: "Output tokens", description: "Output tokens generated.", valueType: "counter", unit: "tokens", direction: "neutral", aggregation: "sum", visibility: "team_visible", boundedDimensions: ["split", "source"] }),
|
|
20
|
+
definition({ id: "runtime.latency_ms", displayName: "Runtime latency", description: "Wall-clock latency of the measured operation.", valueType: "distribution", unit: "milliseconds", direction: "lower", aggregation: "p95", visibility: "team_visible", boundedDimensions: ["operation", "provider"] }),
|
|
21
|
+
definition({ id: "runtime.throughput", displayName: "Token throughput", description: "Tokens processed per second.", valueType: "gauge", unit: "scalar", direction: "higher", aggregation: "mean", visibility: "team_visible", boundedDimensions: ["operation", "provider"] }),
|
|
22
|
+
definition({ id: "gpu.memory_bytes", displayName: "GPU memory", description: "Peak allocated GPU memory.", valueType: "gauge", unit: "bytes", direction: "neutral", aggregation: "max", visibility: "host_private", boundedDimensions: ["provider", "gpuType"] }),
|
|
23
|
+
definition({ id: "gpu.utilization", displayName: "GPU utilization", description: "Observed GPU utilization ratio.", valueType: "gauge", unit: "ratio", direction: "neutral", aggregation: "mean", visibility: "host_private", boundedDimensions: ["provider", "gpuType"] }),
|
|
24
|
+
definition({ id: "cost.usd", displayName: "Run cost", description: "Accrued hosted execution cost.", valueType: "counter", unit: "usd", direction: "lower", aggregation: "sum", visibility: "team_visible", boundedDimensions: ["provider", "resource"] }),
|
|
25
|
+
];
|
|
26
|
+
const catalog = new Map(CORE_METRIC_CATALOG.map((item) => [item.id, item]));
|
|
27
|
+
export function getCoreMetricDefinition(metricId) {
|
|
28
|
+
return catalog.get(metricId);
|
|
29
|
+
}
|
|
30
|
+
export function validateCoreMetricObservation(input) {
|
|
31
|
+
return validateMetricObservation(input, CORE_METRIC_CATALOG);
|
|
32
|
+
}
|
|
33
|
+
export function validateMetricObservation(input, definitions) {
|
|
34
|
+
MetricCatalogSchema.parse(definitions);
|
|
35
|
+
const metric = definitions.find((item) => item.id === input.metricId);
|
|
36
|
+
if (!metric)
|
|
37
|
+
throw new Error(`Unknown metric: ${input.metricId}`);
|
|
38
|
+
const unexpected = Object.keys(input.dimensions).filter((dimension) => !metric.boundedDimensions.includes(dimension));
|
|
39
|
+
if (unexpected.length)
|
|
40
|
+
throw new Error(`Metric ${input.metricId} has unsupported dimensions: ${unexpected.join(", ")}`);
|
|
41
|
+
if (metric.unit === "ratio" && (input.value < 0 || input.value > 1)) {
|
|
42
|
+
throw new Error(`Ratio metric ${input.metricId} must be between zero and one.`);
|
|
43
|
+
}
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
export const MetricCatalogSchema = z.array(MetricDefinitionSchema).min(1).superRefine((items, context) => {
|
|
47
|
+
if (new Set(items.map((item) => item.id)).size !== items.length) {
|
|
48
|
+
context.addIssue({ code: "custom", message: "Metric catalog contains duplicate ids." });
|
|
49
|
+
}
|
|
50
|
+
});
|