276 lines
8.3 KiB
TypeScript
276 lines
8.3 KiB
TypeScript
export const USAGE_SCHEMA_VERSION = 1 as const;
|
|
export const MAX_SAFE_TOKEN_INTEGER = Number.MAX_SAFE_INTEGER;
|
|
|
|
export const USAGE_SCOPES = [
|
|
"main",
|
|
"sync_subagent",
|
|
"async_subagent",
|
|
"tool_selector",
|
|
"summarizer",
|
|
"memory",
|
|
"scheduler",
|
|
"autoskills",
|
|
"diagnostic",
|
|
"skill_eval",
|
|
"unattributed",
|
|
] as const;
|
|
|
|
export type UsageScope = (typeof USAGE_SCOPES)[number];
|
|
export type UsageStatus = "confirmed" | "unknown";
|
|
export type TokenDetails = Record<string, number>;
|
|
|
|
export interface UsageEventV1 {
|
|
schema_version: 1;
|
|
event_id: string;
|
|
event_type: "usage_observed";
|
|
source: "callback_final";
|
|
authority_class: "observed_final";
|
|
revision: 1;
|
|
deployment_id: string;
|
|
workspace_id: string;
|
|
model_call_id: string;
|
|
parent_run_id: string | null;
|
|
provider_request_id: string | null;
|
|
thread_id: string | null;
|
|
source_session_id: string | null;
|
|
turn_id: string | null;
|
|
workspace_dir: string | null;
|
|
scope: UsageScope;
|
|
source_agent: string | null;
|
|
provider_profile_id: string;
|
|
provider_revision: string | null;
|
|
provider_adapter: string;
|
|
model_alias: string;
|
|
upstream_model_id: string;
|
|
usage_status: UsageStatus;
|
|
input_tokens: number | null;
|
|
output_tokens: number | null;
|
|
provider_total_tokens: number | null;
|
|
input_token_details: TokenDetails;
|
|
output_token_details: TokenDetails;
|
|
started_at: string | null;
|
|
observed_at: string;
|
|
completed_at: string;
|
|
}
|
|
|
|
export interface UsageHeartbeatV1 {
|
|
deployment_id: string;
|
|
workspace_id: string;
|
|
emitter_version: string;
|
|
schema_version: 1;
|
|
sender_status: "healthy" | "degraded";
|
|
spool_pending: number;
|
|
spool_inflight: number;
|
|
spool_quarantined: number;
|
|
spool_bytes: number;
|
|
first_loss_at: string | null;
|
|
tracking_degraded_reason: string | null;
|
|
last_error_code: string | null;
|
|
sent_at: string;
|
|
}
|
|
|
|
export interface ValidationResult<T> {
|
|
value?: T;
|
|
issues: string[];
|
|
}
|
|
|
|
const EVENT_KEYS = new Set<keyof UsageEventV1>([
|
|
"schema_version",
|
|
"event_id",
|
|
"event_type",
|
|
"source",
|
|
"authority_class",
|
|
"revision",
|
|
"deployment_id",
|
|
"workspace_id",
|
|
"model_call_id",
|
|
"parent_run_id",
|
|
"provider_request_id",
|
|
"thread_id",
|
|
"source_session_id",
|
|
"turn_id",
|
|
"workspace_dir",
|
|
"scope",
|
|
"source_agent",
|
|
"provider_profile_id",
|
|
"provider_revision",
|
|
"provider_adapter",
|
|
"model_alias",
|
|
"upstream_model_id",
|
|
"usage_status",
|
|
"input_tokens",
|
|
"output_tokens",
|
|
"provider_total_tokens",
|
|
"input_token_details",
|
|
"output_token_details",
|
|
"started_at",
|
|
"observed_at",
|
|
"completed_at",
|
|
]);
|
|
|
|
const UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
|
|
|
|
function hasControlCharacter(value: string): boolean {
|
|
return Array.from(value).some((character) => {
|
|
const code = character.charCodeAt(0);
|
|
return code <= 31 || code === 127;
|
|
});
|
|
}
|
|
|
|
function validString(value: unknown, max: number, nullable = false): boolean {
|
|
if (nullable && value === null) return true;
|
|
return (
|
|
typeof value === "string" &&
|
|
value.length > 0 &&
|
|
value.length <= max &&
|
|
!hasControlCharacter(value)
|
|
);
|
|
}
|
|
|
|
function validTimestamp(value: unknown, nullable = false): boolean {
|
|
if (nullable && value === null) return true;
|
|
return (
|
|
typeof value === "string" &&
|
|
UTC_TIMESTAMP.test(value) &&
|
|
!Number.isNaN(Date.parse(value))
|
|
);
|
|
}
|
|
|
|
function validToken(value: unknown, nullable = false): boolean {
|
|
if (nullable && value === null) return true;
|
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
}
|
|
|
|
function validDetails(value: unknown): value is TokenDetails {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
return Object.entries(value).every(
|
|
([key, count]) =>
|
|
key.length > 0 &&
|
|
key.length <= 128 &&
|
|
!hasControlCharacter(key) &&
|
|
validToken(count)
|
|
);
|
|
}
|
|
|
|
export function validateUsageEvent(
|
|
value: unknown
|
|
): ValidationResult<UsageEventV1> {
|
|
const issues: string[] = [];
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return { issues: ["body_must_be_an_object"] };
|
|
}
|
|
const event = value as Record<string, unknown>;
|
|
for (const key of Object.keys(event)) {
|
|
if (!EVENT_KEYS.has(key as keyof UsageEventV1))
|
|
issues.push(`unknown_field:${key}`);
|
|
}
|
|
for (const key of EVENT_KEYS) {
|
|
if (!Object.prototype.hasOwnProperty.call(event, key))
|
|
issues.push(`missing_field:${key}`);
|
|
}
|
|
const constants: Array<[string, unknown, unknown]> = [
|
|
["schema_version", event.schema_version, 1],
|
|
["event_type", event.event_type, "usage_observed"],
|
|
["source", event.source, "callback_final"],
|
|
["authority_class", event.authority_class, "observed_final"],
|
|
["revision", event.revision, 1],
|
|
];
|
|
for (const [key, actual, expected] of constants) {
|
|
if (actual !== expected) issues.push(`invalid_${key}`);
|
|
}
|
|
const ids: Array<[string, number, boolean]> = [
|
|
["event_id", 1024, false],
|
|
["deployment_id", 256, false],
|
|
["workspace_id", 256, false],
|
|
["model_call_id", 256, false],
|
|
["parent_run_id", 256, true],
|
|
["provider_request_id", 256, true],
|
|
["thread_id", 256, true],
|
|
["source_session_id", 256, true],
|
|
["turn_id", 256, true],
|
|
["source_agent", 256, true],
|
|
["provider_revision", 256, true],
|
|
];
|
|
for (const [key, max, nullable] of ids) {
|
|
if (!validString(event[key], max, nullable)) issues.push(`invalid_${key}`);
|
|
}
|
|
if (!validString(event.workspace_dir, 4096, true))
|
|
issues.push("invalid_workspace_dir");
|
|
for (const key of [
|
|
"provider_profile_id",
|
|
"provider_adapter",
|
|
"model_alias",
|
|
"upstream_model_id",
|
|
] as const) {
|
|
if (!validString(event[key], 512)) issues.push(`invalid_${key}`);
|
|
}
|
|
if (!USAGE_SCOPES.includes(event.scope as UsageScope))
|
|
issues.push("invalid_scope");
|
|
if (event.usage_status !== "confirmed" && event.usage_status !== "unknown")
|
|
issues.push("invalid_usage_status");
|
|
if (!validDetails(event.input_token_details))
|
|
issues.push("invalid_input_token_details");
|
|
if (!validDetails(event.output_token_details))
|
|
issues.push("invalid_output_token_details");
|
|
if (!validTimestamp(event.started_at, true))
|
|
issues.push("invalid_started_at");
|
|
if (!validTimestamp(event.observed_at)) issues.push("invalid_observed_at");
|
|
if (!validTimestamp(event.completed_at)) issues.push("invalid_completed_at");
|
|
|
|
if (event.usage_status === "confirmed") {
|
|
if (!validToken(event.input_tokens)) issues.push("invalid_input_tokens");
|
|
if (!validToken(event.output_tokens)) issues.push("invalid_output_tokens");
|
|
if (!validToken(event.provider_total_tokens, true))
|
|
issues.push("invalid_provider_total_tokens");
|
|
if (
|
|
validToken(event.input_tokens) &&
|
|
validToken(event.output_tokens) &&
|
|
(event.input_tokens as number) + (event.output_tokens as number) >
|
|
MAX_SAFE_TOKEN_INTEGER
|
|
) {
|
|
issues.push("token_sum_exceeds_safe_integer");
|
|
}
|
|
} else if (event.usage_status === "unknown") {
|
|
if (
|
|
event.input_tokens !== null ||
|
|
event.output_tokens !== null ||
|
|
event.provider_total_tokens !== null
|
|
) {
|
|
issues.push("unknown_usage_must_have_null_tokens");
|
|
}
|
|
}
|
|
|
|
if (
|
|
validString(event.deployment_id, 256) &&
|
|
validString(event.model_call_id, 256)
|
|
) {
|
|
const expected = `${event.deployment_id}:${event.model_call_id}:callback_final:1`;
|
|
if (event.event_id !== expected) issues.push("event_id_mismatch");
|
|
}
|
|
if (validTimestamp(event.started_at, true) && event.started_at !== null) {
|
|
const started = Date.parse(event.started_at as string);
|
|
if (
|
|
validTimestamp(event.observed_at) &&
|
|
started > Date.parse(event.observed_at as string)
|
|
)
|
|
issues.push("started_after_observed");
|
|
if (
|
|
validTimestamp(event.completed_at) &&
|
|
started > Date.parse(event.completed_at as string)
|
|
)
|
|
issues.push("started_after_completed");
|
|
}
|
|
return issues.length
|
|
? { issues }
|
|
: { value: event as unknown as UsageEventV1, issues };
|
|
}
|
|
|
|
export function canonicalJson(value: unknown): string {
|
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
const entries = Object.entries(value as Record<string, unknown>)
|
|
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
|
|
return `{${entries.join(",")}}`;
|
|
}
|