mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type {
|
||||
ModelInvocationCompletionRecord,
|
||||
ModelInvocationStartRecord,
|
||||
} from '../model-invocation/modelInvocation';
|
||||
import {
|
||||
normalizeModelInvocationCompletionRecord,
|
||||
normalizeModelInvocationStartRecord,
|
||||
} from '../model-invocation/modelInvocation';
|
||||
|
||||
// Usage owns immutable metering facts derived from completed model invocations.
|
||||
|
||||
export const MODEL_INVOCATION_USAGE_LEDGER_SCHEMA =
|
||||
'qinglong/model-invocation-usage-ledger@v1';
|
||||
export const MAX_MODEL_INVOCATION_USAGE_LEDGER_PAGE_SIZE = 128;
|
||||
export const MAX_MODEL_INVOCATION_USAGE_SUMMARY_ROWS = 100_000;
|
||||
export const MAX_MODEL_INVOCATION_USAGE_QUERY_WINDOW_MS =
|
||||
366 * 24 * 60 * 60_000;
|
||||
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const LEDGER_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/model-invocation-usage-ledger-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
export interface ModelInvocationUsageLedgerRecord {
|
||||
readonly schema: typeof MODEL_INVOCATION_USAGE_LEDGER_SCHEMA;
|
||||
readonly invocationId: string;
|
||||
readonly projectId: string;
|
||||
readonly runId: string;
|
||||
readonly stepRunId: string;
|
||||
readonly traceId: string;
|
||||
readonly provider: string;
|
||||
readonly model: string;
|
||||
readonly policyRevision: string;
|
||||
readonly completionDigest: string;
|
||||
readonly outcome: ModelInvocationCompletionRecord['outcome'];
|
||||
readonly settledAtMs: number;
|
||||
readonly inputBytes: number;
|
||||
readonly outputBytes: number;
|
||||
readonly inputTokens: number;
|
||||
readonly outputTokens: number;
|
||||
readonly totalTokens: number;
|
||||
readonly costMicros: number | null;
|
||||
readonly ledgerDigest: string;
|
||||
}
|
||||
|
||||
export interface ModelInvocationUsageLedgerCursor {
|
||||
readonly settledAtMs: number;
|
||||
readonly invocationId: string;
|
||||
}
|
||||
|
||||
export interface ModelInvocationUsageLedgerQuery {
|
||||
readonly projectId: string;
|
||||
readonly fromMsInclusive: number;
|
||||
readonly toMsExclusive: number;
|
||||
readonly limit: number;
|
||||
readonly after?: Readonly<ModelInvocationUsageLedgerCursor>;
|
||||
}
|
||||
|
||||
export interface ModelInvocationUsageLedgerSummaryQuery {
|
||||
readonly projectId: string;
|
||||
readonly fromMsInclusive: number;
|
||||
readonly toMsExclusive: number;
|
||||
}
|
||||
|
||||
export interface ModelInvocationUsageLedgerPage {
|
||||
readonly records: readonly Readonly<ModelInvocationUsageLedgerRecord>[];
|
||||
readonly hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface ModelInvocationUsageLedgerSummary {
|
||||
readonly invocationCount: number;
|
||||
readonly inputTokens: number;
|
||||
readonly outputTokens: number;
|
||||
readonly totalTokens: number;
|
||||
readonly knownCostMicros: number;
|
||||
readonly unknownCostInvocations: number;
|
||||
}
|
||||
|
||||
export interface ModelInvocationUsageLedgerRepository {
|
||||
findUsage(
|
||||
invocationId: string,
|
||||
): Promise<Readonly<ModelInvocationUsageLedgerRecord> | null>;
|
||||
listProjectUsage(
|
||||
query: ModelInvocationUsageLedgerQuery,
|
||||
): Promise<Readonly<ModelInvocationUsageLedgerPage>>;
|
||||
summarizeProjectUsage(
|
||||
query: ModelInvocationUsageLedgerSummaryQuery,
|
||||
): Promise<Readonly<ModelInvocationUsageLedgerSummary>>;
|
||||
}
|
||||
|
||||
export class InvalidModelInvocationUsageLedgerError extends TypeError {
|
||||
readonly code = 'MODEL_INVOCATION_USAGE_LEDGER_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Model invocation usage ledger is invalid: ${message}`);
|
||||
this.name = 'InvalidModelInvocationUsageLedgerError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelInvocationUsageSummaryLimitExceededError extends Error {
|
||||
readonly code = 'MODEL_INVOCATION_USAGE_SUMMARY_LIMIT_EXCEEDED';
|
||||
|
||||
constructor() {
|
||||
super('Model invocation usage summary exceeds its reviewed row limit');
|
||||
this.name = 'ModelInvocationUsageSummaryLimitExceededError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidModelInvocationUsageLedgerError(message);
|
||||
}
|
||||
|
||||
function dataRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.getPrototypeOf(value) !== Object.prototype
|
||||
) {
|
||||
return invalid(`${label} must be a plain object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(
|
||||
value: object,
|
||||
expected: readonly string[],
|
||||
label: string,
|
||||
): void {
|
||||
const actual = Object.keys(value).sort();
|
||||
const canonical = [...expected].sort();
|
||||
if (
|
||||
actual.length !== canonical.length ||
|
||||
actual.some((key, index) => key !== canonical[index])
|
||||
) {
|
||||
invalid(`${label} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function identifier(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: unknown,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
label: string,
|
||||
): number {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
(value as number) < minimum ||
|
||||
(value as number) > maximum
|
||||
) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function digest(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function hash(value: object): string {
|
||||
return createHash('sha256')
|
||||
.update(LEDGER_DIGEST_DOMAIN)
|
||||
.update(JSON.stringify(value), 'utf8')
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function withoutDigest(
|
||||
value: Readonly<ModelInvocationUsageLedgerRecord>,
|
||||
): Omit<ModelInvocationUsageLedgerRecord, 'ledgerDigest'> {
|
||||
const { ledgerDigest: _ledgerDigest, ...unsigned } = value;
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationUsageLedgerRecord(
|
||||
value: ModelInvocationUsageLedgerRecord,
|
||||
): Readonly<ModelInvocationUsageLedgerRecord> {
|
||||
const candidate = dataRecord(value, 'ledger record');
|
||||
exactKeys(
|
||||
candidate,
|
||||
[
|
||||
'completionDigest',
|
||||
'costMicros',
|
||||
'inputBytes',
|
||||
'inputTokens',
|
||||
'invocationId',
|
||||
'ledgerDigest',
|
||||
'model',
|
||||
'outcome',
|
||||
'outputBytes',
|
||||
'outputTokens',
|
||||
'policyRevision',
|
||||
'projectId',
|
||||
'provider',
|
||||
'runId',
|
||||
'schema',
|
||||
'settledAtMs',
|
||||
'stepRunId',
|
||||
'totalTokens',
|
||||
'traceId',
|
||||
],
|
||||
'ledger record',
|
||||
);
|
||||
if (
|
||||
value.schema !== MODEL_INVOCATION_USAGE_LEDGER_SCHEMA ||
|
||||
!['succeeded', 'failed', 'timed_out', 'outcome_unknown'].includes(
|
||||
value.outcome,
|
||||
)
|
||||
) {
|
||||
invalid('schema or outcome is invalid');
|
||||
}
|
||||
const inputTokens = integer(
|
||||
value.inputTokens,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'input token count',
|
||||
);
|
||||
const outputTokens = integer(
|
||||
value.outputTokens,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'output token count',
|
||||
);
|
||||
const totalTokens = integer(
|
||||
value.totalTokens,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'total token count',
|
||||
);
|
||||
if (totalTokens !== inputTokens + outputTokens) {
|
||||
invalid('total token count is inconsistent');
|
||||
}
|
||||
const normalized = Object.freeze({
|
||||
schema: MODEL_INVOCATION_USAGE_LEDGER_SCHEMA,
|
||||
invocationId: identifier(value.invocationId, 'invocation id'),
|
||||
projectId: identifier(value.projectId, 'Project id'),
|
||||
runId: identifier(value.runId, 'Run id'),
|
||||
stepRunId: identifier(value.stepRunId, 'StepRun id'),
|
||||
traceId: identifier(value.traceId, 'trace id'),
|
||||
provider: identifier(value.provider, 'provider'),
|
||||
model: identifier(value.model, 'model'),
|
||||
policyRevision: identifier(value.policyRevision, 'policy revision'),
|
||||
completionDigest: digest(value.completionDigest, 'completion digest'),
|
||||
outcome: value.outcome,
|
||||
settledAtMs: integer(
|
||||
value.settledAtMs,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'settlement time',
|
||||
),
|
||||
inputBytes: integer(value.inputBytes, 1, 256 * 1024, 'input bytes'),
|
||||
outputBytes: integer(value.outputBytes, 0, 1024 * 1024, 'output bytes'),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
totalTokens,
|
||||
costMicros:
|
||||
value.costMicros === null
|
||||
? null
|
||||
: integer(value.costMicros, 0, Number.MAX_SAFE_INTEGER, 'cost'),
|
||||
ledgerDigest: digest(value.ledgerDigest, 'ledger digest'),
|
||||
});
|
||||
if (hash(withoutDigest(normalized)) !== normalized.ledgerDigest) {
|
||||
invalid('ledger digest is invalid');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function createModelInvocationUsageLedgerRecord(
|
||||
startValue: ModelInvocationStartRecord,
|
||||
completionValue: ModelInvocationCompletionRecord,
|
||||
): Readonly<ModelInvocationUsageLedgerRecord> | null {
|
||||
const start = normalizeModelInvocationStartRecord(startValue);
|
||||
const completion = normalizeModelInvocationCompletionRecord(completionValue);
|
||||
if (
|
||||
completion.invocationId !== start.invocationId ||
|
||||
completion.projectId !== start.projectId ||
|
||||
completion.runId !== start.runId ||
|
||||
completion.stepRunId !== start.stepRunId ||
|
||||
completion.traceId !== start.traceId ||
|
||||
completion.startDigest !== start.startDigest ||
|
||||
completion.completedAtMs < start.admittedAtMs
|
||||
) {
|
||||
invalid('completion is detached from its start');
|
||||
}
|
||||
if (!completion.usage) return null;
|
||||
const unsigned = Object.freeze({
|
||||
schema: MODEL_INVOCATION_USAGE_LEDGER_SCHEMA,
|
||||
invocationId: completion.invocationId,
|
||||
projectId: completion.projectId,
|
||||
runId: completion.runId,
|
||||
stepRunId: completion.stepRunId,
|
||||
traceId: completion.traceId,
|
||||
provider: start.provider,
|
||||
model: start.model,
|
||||
policyRevision: start.policyRevision,
|
||||
completionDigest: completion.completionDigest,
|
||||
outcome: completion.outcome,
|
||||
settledAtMs: completion.completedAtMs,
|
||||
inputBytes: start.inputBytes,
|
||||
outputBytes: completion.outputBytes,
|
||||
inputTokens: completion.usage.inputTokens,
|
||||
outputTokens: completion.usage.outputTokens,
|
||||
totalTokens: completion.usage.totalTokens,
|
||||
costMicros: completion.usage.costMicros ?? null,
|
||||
});
|
||||
return normalizeModelInvocationUsageLedgerRecord({
|
||||
...unsigned,
|
||||
ledgerDigest: hash(unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeWindow(value: Record<string, unknown>): Readonly<{
|
||||
projectId: string;
|
||||
fromMsInclusive: number;
|
||||
toMsExclusive: number;
|
||||
}> {
|
||||
const projectId = identifier(value.projectId, 'Project id');
|
||||
const fromMsInclusive = integer(
|
||||
value.fromMsInclusive,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'window start',
|
||||
);
|
||||
const toMsExclusive = integer(
|
||||
value.toMsExclusive,
|
||||
1,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'window end',
|
||||
);
|
||||
if (
|
||||
toMsExclusive <= fromMsInclusive ||
|
||||
toMsExclusive - fromMsInclusive > MAX_MODEL_INVOCATION_USAGE_QUERY_WINDOW_MS
|
||||
) {
|
||||
invalid('query window is invalid');
|
||||
}
|
||||
return Object.freeze({ projectId, fromMsInclusive, toMsExclusive });
|
||||
}
|
||||
|
||||
function normalizeCursor(
|
||||
value: Readonly<ModelInvocationUsageLedgerCursor>,
|
||||
): Readonly<ModelInvocationUsageLedgerCursor> {
|
||||
const cursor = dataRecord(value, 'ledger cursor');
|
||||
exactKeys(cursor, ['invocationId', 'settledAtMs'], 'ledger cursor');
|
||||
return Object.freeze({
|
||||
settledAtMs: integer(
|
||||
value.settledAtMs,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'cursor settlement time',
|
||||
),
|
||||
invocationId: identifier(value.invocationId, 'cursor invocation id'),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationUsageLedgerQuery(
|
||||
value: ModelInvocationUsageLedgerQuery,
|
||||
): Readonly<ModelInvocationUsageLedgerQuery> {
|
||||
const query = dataRecord(value, 'ledger query');
|
||||
exactKeys(
|
||||
query,
|
||||
value.after === undefined
|
||||
? ['fromMsInclusive', 'limit', 'projectId', 'toMsExclusive']
|
||||
: ['after', 'fromMsInclusive', 'limit', 'projectId', 'toMsExclusive'],
|
||||
'ledger query',
|
||||
);
|
||||
const window = normalizeWindow(query);
|
||||
const after =
|
||||
value.after === undefined ? undefined : normalizeCursor(value.after);
|
||||
if (
|
||||
after &&
|
||||
(after.settledAtMs < window.fromMsInclusive ||
|
||||
after.settledAtMs >= window.toMsExclusive)
|
||||
) {
|
||||
invalid('cursor is outside the query window');
|
||||
}
|
||||
return Object.freeze({
|
||||
...window,
|
||||
limit: integer(
|
||||
value.limit,
|
||||
1,
|
||||
MAX_MODEL_INVOCATION_USAGE_LEDGER_PAGE_SIZE,
|
||||
'query limit',
|
||||
),
|
||||
...(after === undefined ? {} : { after }),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationUsageLedgerSummaryQuery(
|
||||
value: ModelInvocationUsageLedgerSummaryQuery,
|
||||
): Readonly<ModelInvocationUsageLedgerSummaryQuery> {
|
||||
const query = dataRecord(value, 'ledger summary query');
|
||||
exactKeys(
|
||||
query,
|
||||
['fromMsInclusive', 'projectId', 'toMsExclusive'],
|
||||
'ledger summary query',
|
||||
);
|
||||
return normalizeWindow(query);
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type {
|
||||
CommitModelInvocationResult,
|
||||
ModelInvocationCompletionCommand,
|
||||
ModelInvocationCompletionRecord,
|
||||
ModelInvocationRepository,
|
||||
ModelInvocationStartCommand,
|
||||
ModelInvocationStartRecord,
|
||||
} from '../model-invocation/modelInvocation';
|
||||
|
||||
// Usage owns bounded quota reservation and settlement contracts.
|
||||
|
||||
export const MODEL_INVOCATION_QUOTA_ADMISSION_SCHEMA =
|
||||
'qinglong/model-invocation-quota-admission@v1' as const;
|
||||
export const MODEL_INVOCATION_QUOTA_RESERVATION_SCHEMA =
|
||||
'qinglong/model-invocation-quota-reservation@v1' as const;
|
||||
export const MODEL_INVOCATION_QUOTA_SETTLEMENT_SCHEMA =
|
||||
'qinglong/model-invocation-quota-settlement@v1' as const;
|
||||
|
||||
export const MODEL_INVOCATION_QUOTA_WINDOWS_MS = [
|
||||
60_000, 3_600_000, 86_400_000,
|
||||
] as const;
|
||||
export const MAX_MODEL_INVOCATIONS_PER_QUOTA_WINDOW = 100_000;
|
||||
export const MAX_MODEL_TOKENS_PER_QUOTA_WINDOW = 1_000_000_000_000;
|
||||
export const MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW = 1_000_000_000_000_000;
|
||||
|
||||
export interface ModelInvocationProjectQuotaPolicy {
|
||||
readonly revision: string;
|
||||
readonly windowMs: (typeof MODEL_INVOCATION_QUOTA_WINDOWS_MS)[number];
|
||||
readonly maxInvocations: number;
|
||||
readonly maxTokens: number;
|
||||
readonly maxCostMicros: number | null;
|
||||
}
|
||||
|
||||
export interface ModelInvocationQuotaAdmission {
|
||||
readonly schema: typeof MODEL_INVOCATION_QUOTA_ADMISSION_SCHEMA;
|
||||
readonly invocationId: string;
|
||||
readonly projectId: string;
|
||||
readonly modelPolicyRevision: string;
|
||||
readonly quotaPolicyRevision: string;
|
||||
readonly windowMs: number;
|
||||
readonly maxInvocations: number;
|
||||
readonly maxTokens: number;
|
||||
readonly maxCostMicros: number | null;
|
||||
readonly reservedTokens: number;
|
||||
readonly reservedCostMicros: number | null;
|
||||
readonly admissionDigest: string;
|
||||
}
|
||||
|
||||
export interface ModelInvocationQuotaReservation {
|
||||
readonly schema: typeof MODEL_INVOCATION_QUOTA_RESERVATION_SCHEMA;
|
||||
readonly invocationId: string;
|
||||
readonly projectId: string;
|
||||
readonly modelPolicyRevision: string;
|
||||
readonly quotaPolicyRevision: string;
|
||||
readonly windowMs: number;
|
||||
readonly windowStartMs: number;
|
||||
readonly windowEndMs: number;
|
||||
readonly maxInvocations: number;
|
||||
readonly maxTokens: number;
|
||||
readonly maxCostMicros: number | null;
|
||||
readonly reservedTokens: number;
|
||||
readonly reservedCostMicros: number | null;
|
||||
readonly reservedAtMs: number;
|
||||
readonly admissionDigest: string;
|
||||
readonly reservationDigest: string;
|
||||
}
|
||||
|
||||
export interface ModelInvocationQuotaSettlement {
|
||||
readonly schema: typeof MODEL_INVOCATION_QUOTA_SETTLEMENT_SCHEMA;
|
||||
readonly invocationId: string;
|
||||
readonly projectId: string;
|
||||
readonly reservationDigest: string;
|
||||
readonly completionDigest: string;
|
||||
readonly effectiveTokens: number;
|
||||
readonly effectiveCostMicros: number | null;
|
||||
readonly retainedTokenReservation: boolean;
|
||||
readonly retainedCostReservation: boolean;
|
||||
readonly settledAtMs: number;
|
||||
readonly settlementDigest: string;
|
||||
}
|
||||
|
||||
export interface ModelInvocationQuotaWindowUsage {
|
||||
readonly projectId: string;
|
||||
readonly windowStartMs: number;
|
||||
readonly windowEndMs: number;
|
||||
readonly invocationCount: number;
|
||||
readonly effectiveTokens: number;
|
||||
readonly effectiveCostMicros: number;
|
||||
readonly unknownCostInvocations: number;
|
||||
}
|
||||
|
||||
export interface ModelInvocationQuotaRepository {
|
||||
findQuotaReservation(
|
||||
invocationId: string,
|
||||
): Promise<Readonly<ModelInvocationQuotaReservation> | null>;
|
||||
findQuotaSettlement(
|
||||
invocationId: string,
|
||||
): Promise<Readonly<ModelInvocationQuotaSettlement> | null>;
|
||||
readQuotaWindowUsage(
|
||||
projectId: string,
|
||||
atMs?: number,
|
||||
): Promise<Readonly<ModelInvocationQuotaWindowUsage> | null>;
|
||||
}
|
||||
|
||||
export interface QuotaAwareModelInvocationRepository
|
||||
extends ModelInvocationRepository,
|
||||
ModelInvocationQuotaRepository {
|
||||
admitWithQuota(
|
||||
command: ModelInvocationStartCommand,
|
||||
admission: ModelInvocationQuotaAdmission,
|
||||
): Promise<Readonly<CommitModelInvocationResult<ModelInvocationStartRecord>>>;
|
||||
completeWithQuota(
|
||||
command: ModelInvocationCompletionCommand,
|
||||
): Promise<
|
||||
Readonly<CommitModelInvocationResult<ModelInvocationCompletionRecord>>
|
||||
>;
|
||||
}
|
||||
|
||||
export function isQuotaAwareModelInvocationRepository(
|
||||
value: ModelInvocationRepository,
|
||||
): value is QuotaAwareModelInvocationRepository {
|
||||
const candidate = value as Partial<QuotaAwareModelInvocationRepository>;
|
||||
return (
|
||||
typeof candidate.findQuotaReservation === 'function' &&
|
||||
typeof candidate.findQuotaSettlement === 'function' &&
|
||||
typeof candidate.readQuotaWindowUsage === 'function' &&
|
||||
typeof candidate.admitWithQuota === 'function' &&
|
||||
typeof candidate.completeWithQuota === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export class InvalidModelInvocationQuotaError extends TypeError {
|
||||
readonly code = 'MODEL_INVOCATION_QUOTA_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Model invocation quota is invalid: ${message}`);
|
||||
this.name = 'InvalidModelInvocationQuotaError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelInvocationProjectQuotaExceededError extends Error {
|
||||
readonly code = 'MODEL_PROJECT_QUOTA_EXCEEDED';
|
||||
|
||||
constructor() {
|
||||
super('The Project model invocation quota is exhausted');
|
||||
this.name = 'ModelInvocationProjectQuotaExceededError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelInvocationQuotaConfigurationError extends Error {
|
||||
readonly code = 'MODEL_PROJECT_QUOTA_CONFIGURATION_INVALID';
|
||||
|
||||
constructor() {
|
||||
super('The Project model invocation quota cannot safely reserve this call');
|
||||
this.name = 'ModelInvocationQuotaConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const ADMISSION_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/model-invocation-quota-admission-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const RESERVATION_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/model-invocation-quota-reservation-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const SETTLEMENT_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/model-invocation-quota-settlement-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidModelInvocationQuotaError(message);
|
||||
}
|
||||
|
||||
function plainObject(value: unknown, label: string): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.getPrototypeOf(value) !== Object.prototype
|
||||
) {
|
||||
return invalid(`${label} must be a plain object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(
|
||||
value: object,
|
||||
expected: readonly string[],
|
||||
label: string,
|
||||
): void {
|
||||
const actual = Object.keys(value).sort();
|
||||
const canonical = [...expected].sort();
|
||||
if (
|
||||
actual.length !== canonical.length ||
|
||||
actual.some((key, index) => key !== canonical[index])
|
||||
) {
|
||||
invalid(`${label} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function identifier(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: unknown,
|
||||
label: string,
|
||||
minimum: number,
|
||||
maximum = Number.MAX_SAFE_INTEGER,
|
||||
): number {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
(value as number) < minimum ||
|
||||
(value as number) > maximum
|
||||
) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function nullableInteger(
|
||||
value: unknown,
|
||||
label: string,
|
||||
maximum: number,
|
||||
): number | null {
|
||||
return value === null ? null : integer(value, label, 0, maximum);
|
||||
}
|
||||
|
||||
function digest(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function digestRecord(domain: Buffer, value: object): string {
|
||||
return createHash('sha256')
|
||||
.update(domain)
|
||||
.update(JSON.stringify(value))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationProjectQuotaPolicy(
|
||||
value: ModelInvocationProjectQuotaPolicy,
|
||||
): Readonly<ModelInvocationProjectQuotaPolicy> {
|
||||
const record = plainObject(value, 'policy');
|
||||
exactKeys(
|
||||
record,
|
||||
['revision', 'windowMs', 'maxInvocations', 'maxTokens', 'maxCostMicros'],
|
||||
'policy',
|
||||
);
|
||||
const revision = identifier(record.revision, 'policy revision');
|
||||
const windowMs = integer(record.windowMs, 'windowMs', 1);
|
||||
if (
|
||||
!MODEL_INVOCATION_QUOTA_WINDOWS_MS.includes(
|
||||
windowMs as (typeof MODEL_INVOCATION_QUOTA_WINDOWS_MS)[number],
|
||||
)
|
||||
) {
|
||||
invalid('windowMs is unsupported');
|
||||
}
|
||||
return Object.freeze({
|
||||
revision,
|
||||
windowMs: windowMs as (typeof MODEL_INVOCATION_QUOTA_WINDOWS_MS)[number],
|
||||
maxInvocations: integer(
|
||||
record.maxInvocations,
|
||||
'maxInvocations',
|
||||
1,
|
||||
MAX_MODEL_INVOCATIONS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
maxTokens: integer(
|
||||
record.maxTokens,
|
||||
'maxTokens',
|
||||
1,
|
||||
MAX_MODEL_TOKENS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
maxCostMicros: nullableInteger(
|
||||
record.maxCostMicros,
|
||||
'maxCostMicros',
|
||||
MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function createModelInvocationQuotaAdmission(options: {
|
||||
readonly invocationId: string;
|
||||
readonly projectId: string;
|
||||
readonly modelPolicyRevision: string;
|
||||
readonly reservedTokens: number;
|
||||
readonly reservedCostMicros: number | null;
|
||||
readonly quota: ModelInvocationProjectQuotaPolicy;
|
||||
}): Readonly<ModelInvocationQuotaAdmission> {
|
||||
const quota = normalizeModelInvocationProjectQuotaPolicy(options.quota);
|
||||
const reservedTokens = integer(
|
||||
options.reservedTokens,
|
||||
'reservedTokens',
|
||||
1,
|
||||
MAX_MODEL_TOKENS_PER_QUOTA_WINDOW,
|
||||
);
|
||||
const reservedCostMicros = nullableInteger(
|
||||
options.reservedCostMicros,
|
||||
'reservedCostMicros',
|
||||
MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW,
|
||||
);
|
||||
if (quota.maxCostMicros !== null && reservedCostMicros === null) {
|
||||
throw new ModelInvocationQuotaConfigurationError();
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: MODEL_INVOCATION_QUOTA_ADMISSION_SCHEMA,
|
||||
invocationId: identifier(options.invocationId, 'invocationId'),
|
||||
projectId: identifier(options.projectId, 'projectId'),
|
||||
modelPolicyRevision: identifier(
|
||||
options.modelPolicyRevision,
|
||||
'modelPolicyRevision',
|
||||
),
|
||||
quotaPolicyRevision: quota.revision,
|
||||
windowMs: quota.windowMs,
|
||||
maxInvocations: quota.maxInvocations,
|
||||
maxTokens: quota.maxTokens,
|
||||
maxCostMicros: quota.maxCostMicros,
|
||||
reservedTokens,
|
||||
reservedCostMicros,
|
||||
});
|
||||
return Object.freeze({
|
||||
...unsigned,
|
||||
admissionDigest: digestRecord(ADMISSION_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationQuotaAdmission(
|
||||
value: ModelInvocationQuotaAdmission,
|
||||
): Readonly<ModelInvocationQuotaAdmission> {
|
||||
const record = plainObject(value, 'admission');
|
||||
exactKeys(
|
||||
record,
|
||||
[
|
||||
'schema',
|
||||
'invocationId',
|
||||
'projectId',
|
||||
'modelPolicyRevision',
|
||||
'quotaPolicyRevision',
|
||||
'windowMs',
|
||||
'maxInvocations',
|
||||
'maxTokens',
|
||||
'maxCostMicros',
|
||||
'reservedTokens',
|
||||
'reservedCostMicros',
|
||||
'admissionDigest',
|
||||
],
|
||||
'admission',
|
||||
);
|
||||
if (record.schema !== MODEL_INVOCATION_QUOTA_ADMISSION_SCHEMA) {
|
||||
invalid('admission schema is invalid');
|
||||
}
|
||||
const normalized = createModelInvocationQuotaAdmission({
|
||||
invocationId: identifier(record.invocationId, 'invocationId'),
|
||||
projectId: identifier(record.projectId, 'projectId'),
|
||||
modelPolicyRevision: identifier(
|
||||
record.modelPolicyRevision,
|
||||
'modelPolicyRevision',
|
||||
),
|
||||
reservedTokens: integer(
|
||||
record.reservedTokens,
|
||||
'reservedTokens',
|
||||
1,
|
||||
MAX_MODEL_TOKENS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
reservedCostMicros: nullableInteger(
|
||||
record.reservedCostMicros,
|
||||
'reservedCostMicros',
|
||||
MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
quota: {
|
||||
revision: identifier(record.quotaPolicyRevision, 'quotaPolicyRevision'),
|
||||
windowMs: integer(
|
||||
record.windowMs,
|
||||
'windowMs',
|
||||
1,
|
||||
) as ModelInvocationProjectQuotaPolicy['windowMs'],
|
||||
maxInvocations: integer(
|
||||
record.maxInvocations,
|
||||
'maxInvocations',
|
||||
1,
|
||||
MAX_MODEL_INVOCATIONS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
maxTokens: integer(
|
||||
record.maxTokens,
|
||||
'maxTokens',
|
||||
1,
|
||||
MAX_MODEL_TOKENS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
maxCostMicros: nullableInteger(
|
||||
record.maxCostMicros,
|
||||
'maxCostMicros',
|
||||
MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
},
|
||||
});
|
||||
if (
|
||||
digest(record.admissionDigest, 'admissionDigest') !==
|
||||
normalized.admissionDigest
|
||||
) {
|
||||
invalid('admissionDigest is inconsistent');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function createModelInvocationQuotaReservation(
|
||||
admissionValue: ModelInvocationQuotaAdmission,
|
||||
reservedAtMsValue: number,
|
||||
): Readonly<ModelInvocationQuotaReservation> {
|
||||
const admission = normalizeModelInvocationQuotaAdmission(admissionValue);
|
||||
const reservedAtMs = integer(reservedAtMsValue, 'reservedAtMs', 0);
|
||||
const windowStartMs =
|
||||
Math.floor(reservedAtMs / admission.windowMs) * admission.windowMs;
|
||||
const unsigned = Object.freeze({
|
||||
schema: MODEL_INVOCATION_QUOTA_RESERVATION_SCHEMA,
|
||||
invocationId: admission.invocationId,
|
||||
projectId: admission.projectId,
|
||||
modelPolicyRevision: admission.modelPolicyRevision,
|
||||
quotaPolicyRevision: admission.quotaPolicyRevision,
|
||||
windowMs: admission.windowMs,
|
||||
windowStartMs,
|
||||
windowEndMs: windowStartMs + admission.windowMs,
|
||||
maxInvocations: admission.maxInvocations,
|
||||
maxTokens: admission.maxTokens,
|
||||
maxCostMicros: admission.maxCostMicros,
|
||||
reservedTokens: admission.reservedTokens,
|
||||
reservedCostMicros: admission.reservedCostMicros,
|
||||
reservedAtMs,
|
||||
admissionDigest: admission.admissionDigest,
|
||||
});
|
||||
return Object.freeze({
|
||||
...unsigned,
|
||||
reservationDigest: digestRecord(RESERVATION_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationQuotaReservation(
|
||||
value: ModelInvocationQuotaReservation,
|
||||
): Readonly<ModelInvocationQuotaReservation> {
|
||||
const record = plainObject(value, 'reservation');
|
||||
exactKeys(
|
||||
record,
|
||||
[
|
||||
'schema',
|
||||
'invocationId',
|
||||
'projectId',
|
||||
'modelPolicyRevision',
|
||||
'quotaPolicyRevision',
|
||||
'windowMs',
|
||||
'windowStartMs',
|
||||
'windowEndMs',
|
||||
'maxInvocations',
|
||||
'maxTokens',
|
||||
'maxCostMicros',
|
||||
'reservedTokens',
|
||||
'reservedCostMicros',
|
||||
'reservedAtMs',
|
||||
'admissionDigest',
|
||||
'reservationDigest',
|
||||
],
|
||||
'reservation',
|
||||
);
|
||||
if (record.schema !== MODEL_INVOCATION_QUOTA_RESERVATION_SCHEMA) {
|
||||
invalid('reservation schema is invalid');
|
||||
}
|
||||
const admission = normalizeModelInvocationQuotaAdmission({
|
||||
schema: MODEL_INVOCATION_QUOTA_ADMISSION_SCHEMA,
|
||||
invocationId: identifier(record.invocationId, 'invocationId'),
|
||||
projectId: identifier(record.projectId, 'projectId'),
|
||||
modelPolicyRevision: identifier(
|
||||
record.modelPolicyRevision,
|
||||
'modelPolicyRevision',
|
||||
),
|
||||
quotaPolicyRevision: identifier(
|
||||
record.quotaPolicyRevision,
|
||||
'quotaPolicyRevision',
|
||||
),
|
||||
windowMs: integer(record.windowMs, 'windowMs', 1),
|
||||
maxInvocations: integer(
|
||||
record.maxInvocations,
|
||||
'maxInvocations',
|
||||
1,
|
||||
MAX_MODEL_INVOCATIONS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
maxTokens: integer(
|
||||
record.maxTokens,
|
||||
'maxTokens',
|
||||
1,
|
||||
MAX_MODEL_TOKENS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
maxCostMicros: nullableInteger(
|
||||
record.maxCostMicros,
|
||||
'maxCostMicros',
|
||||
MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
reservedTokens: integer(
|
||||
record.reservedTokens,
|
||||
'reservedTokens',
|
||||
1,
|
||||
MAX_MODEL_TOKENS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
reservedCostMicros: nullableInteger(
|
||||
record.reservedCostMicros,
|
||||
'reservedCostMicros',
|
||||
MAX_MODEL_COST_MICROS_PER_QUOTA_WINDOW,
|
||||
),
|
||||
admissionDigest: digest(record.admissionDigest, 'admissionDigest'),
|
||||
});
|
||||
const normalized = createModelInvocationQuotaReservation(
|
||||
admission,
|
||||
integer(record.reservedAtMs, 'reservedAtMs', 0),
|
||||
);
|
||||
if (
|
||||
integer(record.windowStartMs, 'windowStartMs', 0) !==
|
||||
normalized.windowStartMs ||
|
||||
integer(record.windowEndMs, 'windowEndMs', 1) !== normalized.windowEndMs ||
|
||||
digest(record.reservationDigest, 'reservationDigest') !==
|
||||
normalized.reservationDigest
|
||||
) {
|
||||
invalid('reservation is inconsistent');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function createModelInvocationQuotaSettlement(
|
||||
reservationValue: ModelInvocationQuotaReservation,
|
||||
completion: Readonly<ModelInvocationCompletionRecord>,
|
||||
): Readonly<ModelInvocationQuotaSettlement> {
|
||||
const reservation =
|
||||
normalizeModelInvocationQuotaReservation(reservationValue);
|
||||
if (
|
||||
completion.invocationId !== reservation.invocationId ||
|
||||
completion.projectId !== reservation.projectId
|
||||
) {
|
||||
invalid('completion identity does not match reservation');
|
||||
}
|
||||
const usage = completion.usage;
|
||||
const retainedTokenReservation = usage === null;
|
||||
const retainedCostReservation =
|
||||
reservation.reservedCostMicros !== null &&
|
||||
(usage === null || usage.costMicros === undefined);
|
||||
const effectiveTokens = retainedTokenReservation
|
||||
? reservation.reservedTokens
|
||||
: usage.totalTokens;
|
||||
const effectiveCostMicros =
|
||||
reservation.reservedCostMicros === null
|
||||
? null
|
||||
: retainedCostReservation
|
||||
? reservation.reservedCostMicros
|
||||
: usage!.costMicros!;
|
||||
if (
|
||||
effectiveTokens > reservation.reservedTokens ||
|
||||
(reservation.reservedCostMicros !== null &&
|
||||
effectiveCostMicros !== null &&
|
||||
effectiveCostMicros > reservation.reservedCostMicros)
|
||||
) {
|
||||
invalid('completion usage exceeds reservation');
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: MODEL_INVOCATION_QUOTA_SETTLEMENT_SCHEMA,
|
||||
invocationId: reservation.invocationId,
|
||||
projectId: reservation.projectId,
|
||||
reservationDigest: reservation.reservationDigest,
|
||||
completionDigest: completion.completionDigest,
|
||||
effectiveTokens,
|
||||
effectiveCostMicros,
|
||||
retainedTokenReservation,
|
||||
retainedCostReservation,
|
||||
settledAtMs: completion.completedAtMs,
|
||||
});
|
||||
return Object.freeze({
|
||||
...unsigned,
|
||||
settlementDigest: digestRecord(SETTLEMENT_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeModelInvocationQuotaSettlement(
|
||||
value: ModelInvocationQuotaSettlement,
|
||||
reservation: ModelInvocationQuotaReservation,
|
||||
completion: Readonly<ModelInvocationCompletionRecord>,
|
||||
): Readonly<ModelInvocationQuotaSettlement> {
|
||||
const record = plainObject(value, 'settlement');
|
||||
exactKeys(
|
||||
record,
|
||||
[
|
||||
'schema',
|
||||
'invocationId',
|
||||
'projectId',
|
||||
'reservationDigest',
|
||||
'completionDigest',
|
||||
'effectiveTokens',
|
||||
'effectiveCostMicros',
|
||||
'retainedTokenReservation',
|
||||
'retainedCostReservation',
|
||||
'settledAtMs',
|
||||
'settlementDigest',
|
||||
],
|
||||
'settlement',
|
||||
);
|
||||
if (record.schema !== MODEL_INVOCATION_QUOTA_SETTLEMENT_SCHEMA) {
|
||||
invalid('settlement schema is invalid');
|
||||
}
|
||||
const expected = createModelInvocationQuotaSettlement(
|
||||
reservation,
|
||||
completion,
|
||||
);
|
||||
if (
|
||||
Object.keys(expected).some(
|
||||
(key) =>
|
||||
record[key] !== expected[key as keyof ModelInvocationQuotaSettlement],
|
||||
)
|
||||
) {
|
||||
invalid('settlement is inconsistent');
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
Reference in New Issue
Block a user