mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
246 lines
6.6 KiB
JavaScript
246 lines
6.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
InvalidModelPricingError,
|
|
MODEL_INVOCATION_PRICE_QUOTE_SCHEMA,
|
|
MODEL_INVOCATION_PRICE_SETTLEMENT_SCHEMA,
|
|
MODEL_PRICE_CATALOG_ENTRY_SCHEMA,
|
|
StaticModelPriceCatalog,
|
|
createModelInvocationPriceQuote,
|
|
createModelInvocationPriceSettlement,
|
|
createModelPriceCatalogEntry,
|
|
normalizeModelInvocationPriceQuote,
|
|
normalizeModelInvocationPriceSettlement,
|
|
normalizeModelPriceCatalogEntry,
|
|
priceModelUsage,
|
|
} = require('../dist/pricing/pricing.js');
|
|
|
|
const {
|
|
createModelInvocationCompletionCommand,
|
|
createModelInvocationMutationIdentity,
|
|
createModelInvocationStartCommand,
|
|
} = require('../dist/model-invocation/modelInvocation.js');
|
|
const {
|
|
createStepRunRecord,
|
|
transitionStepRunMutation,
|
|
} = require('@qinglong/runtime-core/step-run');
|
|
|
|
const NOW = 1_000_000;
|
|
|
|
function price(overrides = {}) {
|
|
return createModelPriceCatalogEntry({
|
|
provider: 'remote',
|
|
model: 'model-a',
|
|
priceRevision: 'price-2026-07',
|
|
currency: 'USD',
|
|
inputMicrosPerMillionTokens: 150_000,
|
|
outputMicrosPerMillionTokens: 600_000,
|
|
publishedAtMs: NOW - 1,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
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 commands(usage) {
|
|
const failed = usage === null;
|
|
const ready = createStepRunRecord({
|
|
id: 'step-a',
|
|
runId: 'run-a',
|
|
stepKey: 'model',
|
|
kind: 'model',
|
|
definitionRef: 'prompt:a@1',
|
|
definitionDigest: 'a'.repeat(64),
|
|
required: true,
|
|
initialStatus: 'ready',
|
|
inputRef: 'artifact:a',
|
|
mutationId: 'create-step-a',
|
|
createdAtMs: NOW - 1,
|
|
});
|
|
const startIdentity = createModelInvocationMutationIdentity(
|
|
'request-a',
|
|
'start',
|
|
);
|
|
const start = createModelInvocationStartCommand(
|
|
audit('admitted'),
|
|
transitionStepRunMutation(
|
|
ready,
|
|
{
|
|
expectedVersion: ready.version,
|
|
expectedDigest: ready.stepRunDigest,
|
|
mutationId: startIdentity.mutationId,
|
|
to: 'running',
|
|
atMs: NOW,
|
|
},
|
|
{
|
|
expectedRunVersion: 1,
|
|
expectedRunEventSequence: 1,
|
|
eventId: startIdentity.eventId,
|
|
dedupeKey: startIdentity.dedupeKey,
|
|
actor: { type: 'executor', id: 'model-gateway' },
|
|
},
|
|
),
|
|
);
|
|
const completionIdentity = createModelInvocationMutationIdentity(
|
|
'request-a',
|
|
'completion',
|
|
);
|
|
const completion = createModelInvocationCompletionCommand(
|
|
start.start,
|
|
audit(failed ? 'failed' : 'completed', {
|
|
outputBytes: failed ? 0 : 12,
|
|
usage,
|
|
errorCode: failed ? 'MODEL_PROVIDER_FAILED' : null,
|
|
occurredAtMs: NOW + 25,
|
|
}),
|
|
transitionStepRunMutation(
|
|
start.stepRunMutation.stepRun,
|
|
{
|
|
expectedVersion: start.start.startedStepRunVersion,
|
|
expectedDigest: start.start.startedStepRunDigest,
|
|
mutationId: completionIdentity.mutationId,
|
|
to: failed ? 'failed' : 'succeeded',
|
|
...(failed
|
|
? {
|
|
resultCode: 'model_provider_failed',
|
|
errorSummary: 'Model invocation failed',
|
|
}
|
|
: { outputRef: 'model-invocation:request-a' }),
|
|
atMs: NOW + 25,
|
|
},
|
|
{
|
|
expectedRunVersion: 2,
|
|
expectedRunEventSequence: 2,
|
|
eventId: completionIdentity.eventId,
|
|
dedupeKey: completionIdentity.dedupeKey,
|
|
actor: { type: 'executor', id: 'model-gateway' },
|
|
},
|
|
),
|
|
);
|
|
return { start, completion };
|
|
}
|
|
|
|
test('catalog keeps exact immutable provider/model/revision identities', async () => {
|
|
const entry = price();
|
|
const catalog = new StaticModelPriceCatalog([entry]);
|
|
|
|
assert.equal(entry.schema, MODEL_PRICE_CATALOG_ENTRY_SCHEMA);
|
|
assert.deepEqual(normalizeModelPriceCatalogEntry(entry), entry);
|
|
assert.deepEqual(
|
|
await catalog.resolve({
|
|
provider: 'remote',
|
|
model: 'model-a',
|
|
priceRevision: 'price-2026-07',
|
|
}),
|
|
entry,
|
|
);
|
|
assert.equal(
|
|
await catalog.resolve({
|
|
provider: 'remote',
|
|
model: 'model-a',
|
|
priceRevision: 'price-older',
|
|
}),
|
|
null,
|
|
);
|
|
assert.throws(
|
|
() => new StaticModelPriceCatalog([entry, entry]),
|
|
InvalidModelPricingError,
|
|
);
|
|
});
|
|
|
|
test('quote reserves the worst valid token allocation without overflow', () => {
|
|
const quote = createModelInvocationPriceQuote(price(), {
|
|
invocationId: 'request-a',
|
|
projectId: 'project-a',
|
|
modelPolicyRevision: 'policy-1',
|
|
maxTotalTokens: 256,
|
|
maxOutputTokens: 64,
|
|
});
|
|
|
|
assert.equal(quote.schema, MODEL_INVOCATION_PRICE_QUOTE_SCHEMA);
|
|
assert.equal(quote.reservedCostMicros, 68);
|
|
assert.deepEqual(normalizeModelInvocationPriceQuote(quote), quote);
|
|
assert.equal(Object.isFrozen(quote), true);
|
|
});
|
|
|
|
test('settlement deterministically prices exact usage and ignores provider cost', () => {
|
|
const quote = createModelInvocationPriceQuote(price(), {
|
|
invocationId: 'request-a',
|
|
projectId: 'project-a',
|
|
modelPolicyRevision: 'policy-1',
|
|
maxTotalTokens: 256,
|
|
maxOutputTokens: 64,
|
|
});
|
|
const providerUsage = {
|
|
inputTokens: 5,
|
|
outputTokens: 2,
|
|
totalTokens: 7,
|
|
costMicros: 99_999,
|
|
};
|
|
const canonicalUsage = priceModelUsage(quote, providerUsage);
|
|
const { completion } = commands(canonicalUsage);
|
|
const settlement = createModelInvocationPriceSettlement(
|
|
quote,
|
|
completion.completion,
|
|
);
|
|
|
|
assert.ok(settlement);
|
|
assert.equal(canonicalUsage.costMicros, 3);
|
|
assert.equal(settlement.schema, MODEL_INVOCATION_PRICE_SETTLEMENT_SCHEMA);
|
|
assert.equal(settlement.costMicros, 3);
|
|
assert.deepEqual(
|
|
normalizeModelInvocationPriceSettlement(
|
|
settlement,
|
|
quote,
|
|
completion.completion,
|
|
),
|
|
settlement,
|
|
);
|
|
});
|
|
|
|
test('unknown usage remains unpriced and quote tampering fails closed', () => {
|
|
const quote = createModelInvocationPriceQuote(price(), {
|
|
invocationId: 'request-a',
|
|
projectId: 'project-a',
|
|
modelPolicyRevision: 'policy-1',
|
|
maxTotalTokens: 256,
|
|
maxOutputTokens: 64,
|
|
});
|
|
const failed = commands(null);
|
|
|
|
assert.equal(
|
|
createModelInvocationPriceSettlement(quote, failed.completion.completion),
|
|
null,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeModelInvocationPriceQuote({
|
|
...quote,
|
|
reservedCostMicros: quote.reservedCostMicros + 1,
|
|
}),
|
|
InvalidModelPricingError,
|
|
);
|
|
});
|