mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
258 lines
7.4 KiB
JavaScript
258 lines
7.4 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
createStepRunRecord,
|
|
transitionStepRunMutation,
|
|
} = require('@qinglong/runtime-core/step-run');
|
|
const {
|
|
createModelInvocationCompletionCommand,
|
|
createModelInvocationMutationIdentity,
|
|
createModelInvocationStartCommand,
|
|
} = require('../dist/model-invocation/modelInvocation.js');
|
|
const {
|
|
InvalidModelInvocationUsageLedgerError,
|
|
MAX_MODEL_INVOCATION_USAGE_QUERY_WINDOW_MS,
|
|
MODEL_INVOCATION_USAGE_LEDGER_SCHEMA,
|
|
createModelInvocationUsageLedgerRecord,
|
|
normalizeModelInvocationUsageLedgerQuery,
|
|
normalizeModelInvocationUsageLedgerRecord,
|
|
normalizeModelInvocationUsageLedgerSummaryQuery,
|
|
} = require('@qinglong/ai/usage-ledger');
|
|
|
|
const NOW = 1_000_000;
|
|
|
|
function audit(phase, overrides = {}) {
|
|
return {
|
|
phase,
|
|
projectId: 'project-a',
|
|
runId: 'run-a',
|
|
stepRunId: 'step-a',
|
|
traceId: 'trace-a',
|
|
requestId: 'request-a',
|
|
provider: 'remote',
|
|
model: 'model-a',
|
|
policyRevision: 'policy-1',
|
|
requestDigest: `sha256:${'b'.repeat(64)}`,
|
|
deadlineAtMs: NOW + 10_000,
|
|
inputBytes: 128,
|
|
maxOutputTokens: 64,
|
|
outputBytes: 0,
|
|
usage: null,
|
|
errorCode: null,
|
|
occurredAtMs: NOW,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function startFixture(projectId = 'project-a') {
|
|
const current = createStepRunRecord({
|
|
id: 'step-a',
|
|
runId: 'run-a',
|
|
stepKey: 'summarize',
|
|
kind: 'model',
|
|
definitionRef: 'prompt:summary@1',
|
|
definitionDigest: 'a'.repeat(64),
|
|
required: true,
|
|
initialStatus: 'ready',
|
|
inputRef: 'artifact:input-a',
|
|
mutationId: 'create-step-a',
|
|
createdAtMs: NOW - 1,
|
|
});
|
|
const identity = createModelInvocationMutationIdentity('request-a', 'start');
|
|
const mutation = transitionStepRunMutation(
|
|
current,
|
|
{
|
|
expectedVersion: current.version,
|
|
expectedDigest: current.stepRunDigest,
|
|
mutationId: identity.mutationId,
|
|
to: 'running',
|
|
atMs: NOW,
|
|
},
|
|
{
|
|
expectedRunVersion: 1,
|
|
expectedRunEventSequence: 1,
|
|
eventId: identity.eventId,
|
|
dedupeKey: identity.dedupeKey,
|
|
actor: { type: 'executor', id: 'model-gateway' },
|
|
},
|
|
);
|
|
return createModelInvocationStartCommand(
|
|
audit('admitted', { projectId }),
|
|
mutation,
|
|
);
|
|
}
|
|
|
|
function completionFixture({
|
|
errorCode = null,
|
|
usage = { inputTokens: 5, outputTokens: 2, totalTokens: 7, costMicros: 11 },
|
|
} = {}) {
|
|
const startCommand = startFixture();
|
|
const identity = createModelInvocationMutationIdentity(
|
|
'request-a',
|
|
'completion',
|
|
);
|
|
const failed = errorCode !== null;
|
|
const mutation = transitionStepRunMutation(
|
|
startCommand.stepRunMutation.stepRun,
|
|
{
|
|
expectedVersion: startCommand.start.startedStepRunVersion,
|
|
expectedDigest: startCommand.start.startedStepRunDigest,
|
|
mutationId: identity.mutationId,
|
|
to: failed ? 'failed' : 'succeeded',
|
|
atMs: NOW + 25,
|
|
...(failed
|
|
? {
|
|
resultCode: 'model_provider_failed',
|
|
errorSummary: 'Model invocation failed',
|
|
}
|
|
: { outputRef: 'model-invocation:request-a' }),
|
|
},
|
|
{
|
|
expectedRunVersion: 2,
|
|
expectedRunEventSequence: 2,
|
|
eventId: identity.eventId,
|
|
dedupeKey: identity.dedupeKey,
|
|
actor: { type: 'executor', id: 'model-gateway' },
|
|
},
|
|
);
|
|
return createModelInvocationCompletionCommand(
|
|
startCommand.start,
|
|
audit(failed ? 'failed' : 'completed', {
|
|
occurredAtMs: NOW + 25,
|
|
outputBytes: failed ? 0 : 12,
|
|
usage,
|
|
errorCode,
|
|
}),
|
|
mutation,
|
|
);
|
|
}
|
|
|
|
test('usage ledger derives one immutable content-free billing fact', () => {
|
|
const start = startFixture().start;
|
|
const completion = completionFixture().completion;
|
|
const ledger = createModelInvocationUsageLedgerRecord(start, completion);
|
|
|
|
assert.ok(ledger);
|
|
assert.equal(ledger.schema, MODEL_INVOCATION_USAGE_LEDGER_SCHEMA);
|
|
assert.equal(ledger.invocationId, completion.invocationId);
|
|
assert.equal(ledger.completionDigest, completion.completionDigest);
|
|
assert.equal(ledger.provider, start.provider);
|
|
assert.equal(ledger.model, start.model);
|
|
assert.equal(ledger.policyRevision, start.policyRevision);
|
|
assert.equal(ledger.outcome, 'succeeded');
|
|
assert.equal(ledger.inputTokens, 5);
|
|
assert.equal(ledger.outputTokens, 2);
|
|
assert.equal(ledger.totalTokens, 7);
|
|
assert.equal(ledger.costMicros, 11);
|
|
assert.deepEqual(
|
|
createModelInvocationUsageLedgerRecord(start, completion),
|
|
ledger,
|
|
);
|
|
assert.deepEqual(normalizeModelInvocationUsageLedgerRecord(ledger), ledger);
|
|
assert.equal(Object.isFrozen(ledger), true);
|
|
assert.equal(JSON.stringify(ledger).includes('top secret prompt'), false);
|
|
});
|
|
|
|
test('failed provider completion remains billable when usage is known', () => {
|
|
const start = startFixture().start;
|
|
const completion = completionFixture({
|
|
errorCode: 'MODEL_PROVIDER_FAILED',
|
|
usage: { inputTokens: 5, outputTokens: 0, totalTokens: 5 },
|
|
}).completion;
|
|
const ledger = createModelInvocationUsageLedgerRecord(start, completion);
|
|
|
|
assert.ok(ledger);
|
|
assert.equal(ledger.outcome, 'failed');
|
|
assert.equal(ledger.totalTokens, 5);
|
|
assert.equal(ledger.costMicros, null);
|
|
});
|
|
|
|
test('completion without usage creates no synthetic zero-cost fact', () => {
|
|
const start = startFixture().start;
|
|
const completion = completionFixture({
|
|
errorCode: 'MODEL_PROVIDER_FAILED',
|
|
usage: null,
|
|
}).completion;
|
|
|
|
assert.equal(createModelInvocationUsageLedgerRecord(start, completion), null);
|
|
});
|
|
|
|
test('detached completion and digest tampering fail closed', () => {
|
|
const start = startFixture().start;
|
|
const completion = completionFixture().completion;
|
|
const ledger = createModelInvocationUsageLedgerRecord(start, completion);
|
|
|
|
assert.throws(
|
|
() =>
|
|
createModelInvocationUsageLedgerRecord(
|
|
startFixture('project-b').start,
|
|
completion,
|
|
),
|
|
InvalidModelInvocationUsageLedgerError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeModelInvocationUsageLedgerRecord({
|
|
...ledger,
|
|
totalTokens: ledger.totalTokens + 1,
|
|
}),
|
|
InvalidModelInvocationUsageLedgerError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeModelInvocationUsageLedgerRecord({
|
|
...ledger,
|
|
ledgerDigest: '0'.repeat(64),
|
|
}),
|
|
InvalidModelInvocationUsageLedgerError,
|
|
);
|
|
});
|
|
|
|
test('Project usage query is bounded and cursor stays inside its window', () => {
|
|
const query = {
|
|
projectId: 'project-a',
|
|
fromMsInclusive: NOW,
|
|
toMsExclusive: NOW + 100,
|
|
limit: 128,
|
|
after: { settledAtMs: NOW + 25, invocationId: 'request-a' },
|
|
};
|
|
|
|
assert.deepEqual(normalizeModelInvocationUsageLedgerQuery(query), query);
|
|
assert.deepEqual(
|
|
normalizeModelInvocationUsageLedgerSummaryQuery({
|
|
projectId: 'project-a',
|
|
fromMsInclusive: NOW,
|
|
toMsExclusive: NOW + 100,
|
|
}),
|
|
{
|
|
projectId: 'project-a',
|
|
fromMsInclusive: NOW,
|
|
toMsExclusive: NOW + 100,
|
|
},
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeModelInvocationUsageLedgerQuery({
|
|
...query,
|
|
toMsExclusive:
|
|
query.fromMsInclusive +
|
|
MAX_MODEL_INVOCATION_USAGE_QUERY_WINDOW_MS +
|
|
1,
|
|
}),
|
|
InvalidModelInvocationUsageLedgerError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeModelInvocationUsageLedgerQuery({
|
|
...query,
|
|
after: { ...query.after, settledAtMs: NOW - 1 },
|
|
}),
|
|
InvalidModelInvocationUsageLedgerError,
|
|
);
|
|
assert.throws(
|
|
() => normalizeModelInvocationUsageLedgerQuery({ ...query, limit: 129 }),
|
|
InvalidModelInvocationUsageLedgerError,
|
|
);
|
|
});
|