maka-agent 0.2.0-dev.74.20261008 → 0.2.0-dev.76.20261009
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/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +3 -1
- package/node_modules/@maka/runtime-host/dist/protocol/usage-pricing.js +12 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +54 -13
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-runner.js +13 -6
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +10 -0
- package/node_modules/@maka/storage/dist/model-call-ledger.js +59 -1
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +38 -0
- package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +17 -2
- package/node_modules/@maka/storage/dist/sqlite-usage-store.js +4 -0
- package/node_modules/@maka/storage/dist/usage-stores.js +3 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -65,7 +65,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
|
|
|
65
65
|
export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
|
|
66
66
|
// Increment when the same protocol version no longer guarantees safe Client-Host
|
|
67
67
|
// interoperability. Mismatches are rejected before domain commands are admitted.
|
|
68
|
-
export const RUNTIME_HOST_COMPATIBILITY_EPOCH =
|
|
68
|
+
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 208;
|
|
69
|
+
// 208: Usage queries filter by model call kind. Epoch-207 peers reject the
|
|
70
|
+
// filter or ignore it.
|
|
69
71
|
// 206: `storage.retention.query` and `storage.retention.set` read and change the
|
|
70
72
|
// opt-in retention for archived tasks. An epoch-205 Client could not show or
|
|
71
73
|
// turn off a Host that deletes archived tasks on its own, and an epoch-205 Host
|
|
@@ -44,6 +44,7 @@ const LLM_USAGE_QUERY_FIELDS = new Set([
|
|
|
44
44
|
'providerId',
|
|
45
45
|
'modelId',
|
|
46
46
|
'status',
|
|
47
|
+
'callKinds',
|
|
47
48
|
]);
|
|
48
49
|
const TOOL_USAGE_QUERY_FIELDS = new Set(['range', 'toolName', 'status']);
|
|
49
50
|
const USAGE_BUCKET_FIELDS = new Set([
|
|
@@ -457,8 +458,19 @@ function decodeLlmUsageQuery(value) {
|
|
|
457
458
|
...optionalQueryText(query, 'providerId'),
|
|
458
459
|
...optionalQueryText(query, 'modelId'),
|
|
459
460
|
...(query.status === undefined ? {} : { status: decodeUsageStatus(query.status) }),
|
|
461
|
+
...(query.callKinds === undefined ? {} : { callKinds: decodeCallKinds(query.callKinds) }),
|
|
460
462
|
};
|
|
461
463
|
}
|
|
464
|
+
function decodeCallKinds(value) {
|
|
465
|
+
if (!Array.isArray(value))
|
|
466
|
+
throw invalidProtocolFrame('Invalid usage query callKinds');
|
|
467
|
+
return value.map((entry) => {
|
|
468
|
+
if (typeof entry !== 'string' || !MODEL_CALL_KINDS.includes(entry)) {
|
|
469
|
+
throw invalidProtocolFrame('Invalid usage query callKind');
|
|
470
|
+
}
|
|
471
|
+
return entry;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
462
474
|
function decodeToolUsageQuery(value) {
|
|
463
475
|
const query = requireRecord(value, 'tool usage query');
|
|
464
476
|
assertAllowedKeys(query, TOOL_USAGE_QUERY_FIELDS, 'tool usage query');
|
|
@@ -2288,6 +2288,7 @@ export async function createExecutionRuntimeHostComposition(context, options = {
|
|
|
2288
2288
|
'turn.stop': turnControl.handlers['turn.stop'],
|
|
2289
2289
|
'usage.query': usagePricing.handlers['usage.query'],
|
|
2290
2290
|
},
|
|
2291
|
+
runSettlementCoverage: (from, to) => usagePricing.runSettlementCoverage(from, to),
|
|
2291
2292
|
context: {
|
|
2292
2293
|
hostEpoch: context.hostEpoch,
|
|
2293
2294
|
connectionId: 'hosted-execution',
|
|
@@ -31,6 +31,7 @@ import { buildSessionTitlePrompt, cleanGeneratedSessionTitle, SESSION_TITLE_GENE
|
|
|
31
31
|
import { createProxiedFetchTransport, } from '@maka/runtime/network/scoped-fetch-transport';
|
|
32
32
|
import { generateToolFreeModelCall, generateProviderPrefixModelCall, ProviderPrefixModelCallUnavailableError, } from '@maka/runtime/tool-free-model-call';
|
|
33
33
|
import { resolveModelRuntime } from '@maka/runtime/model-runtime';
|
|
34
|
+
import { NO_RUN_TURN_ID } from '@maka/storage/model-call-ledger';
|
|
34
35
|
import { memoryExtractionMaxOutputTokens, } from '@maka/runtime/memory-extraction';
|
|
35
36
|
import { createHostOAuthModelFetch, OAuthExecutionCredentialError, } from './oauth-execution-authority.js';
|
|
36
37
|
import { toRuntimePolicyProxy } from './runtime-policy-proxy.js';
|
|
@@ -304,6 +305,15 @@ export function createHostGoalEvaluator(input) {
|
|
|
304
305
|
},
|
|
305
306
|
});
|
|
306
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Turn placeholder for canonical rows of calls no run owns: auxiliary calls
|
|
310
|
+
* happen outside any turn, and the ledger's countable rows require the column.
|
|
311
|
+
* The storage side records the rows' no-run ownership from this seam, so a
|
|
312
|
+
* failed auxiliary call cannot flip a hosted run to indeterminate (#5691) —
|
|
313
|
+
* including a hosted execution legally named like this placeholder
|
|
314
|
+
* (#5890 review).
|
|
315
|
+
*/
|
|
316
|
+
const AUXILIARY_TURN_ID = NO_RUN_TURN_ID;
|
|
307
317
|
function createAuxiliaryModelCallAuthority(input) {
|
|
308
318
|
let drainRequested = false;
|
|
309
319
|
const requestDrain = () => {
|
|
@@ -328,6 +338,17 @@ function createAuxiliaryModelCallAuthority(input) {
|
|
|
328
338
|
}
|
|
329
339
|
},
|
|
330
340
|
},
|
|
341
|
+
modelCalls: {
|
|
342
|
+
recordUsageUnknownAttempt: async (record) => {
|
|
343
|
+
try {
|
|
344
|
+
await input.usage.modelCalls.recordUsageUnknownAttempt(record);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
requestDrain();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
},
|
|
331
352
|
requestDrain,
|
|
332
353
|
now: input.now ?? Date.now,
|
|
333
354
|
newId: input.newId ?? randomUUID,
|
|
@@ -373,6 +394,32 @@ async function runHostAuxiliaryModelCall(authority, input) {
|
|
|
373
394
|
modelId: target.model,
|
|
374
395
|
startedAt,
|
|
375
396
|
};
|
|
397
|
+
// Abort and error paths know no token counts: their canonical row says
|
|
398
|
+
// usage-unknown (`usageBasis: 'missing'`, no tokens) instead of posing as a
|
|
399
|
+
// free call (#5691). Auxiliary calls run outside any AgentRun, so the
|
|
400
|
+
// canonical writer is this store seam, not the run's event stream.
|
|
401
|
+
const recordUsageUnknown = async (status, errorClass) => {
|
|
402
|
+
const completedAt = authority.now();
|
|
403
|
+
try {
|
|
404
|
+
await authority.modelCalls.recordUsageUnknownAttempt({
|
|
405
|
+
attemptId: input.callId,
|
|
406
|
+
completedAt,
|
|
407
|
+
...(input.telemetrySessionId ? { sessionId: input.telemetrySessionId } : {}),
|
|
408
|
+
logicalCallId: input.callId,
|
|
409
|
+
turnId: AUXILIARY_TURN_ID,
|
|
410
|
+
callKind: input.callKind,
|
|
411
|
+
connectionSlug: target.connection.slug,
|
|
412
|
+
providerId: target.connection.providerType,
|
|
413
|
+
modelId: target.model,
|
|
414
|
+
latencyMs: Math.max(0, completedAt - startedAt),
|
|
415
|
+
status,
|
|
416
|
+
...(errorClass ? { errorClass } : {}),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (accountingError) {
|
|
420
|
+
throw new AuxiliaryModelCallLocalError('accounting', accountingError);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
376
423
|
let result;
|
|
377
424
|
try {
|
|
378
425
|
result = await readDuringBackendCreation(() => {
|
|
@@ -408,24 +455,18 @@ async function runHostAuxiliaryModelCall(authority, input) {
|
|
|
408
455
|
}
|
|
409
456
|
catch (error) {
|
|
410
457
|
const effectiveError = readDeferredOAuthFailure?.() ?? error;
|
|
411
|
-
|
|
412
|
-
await recordLlmCallStrict({ repo: authority.telemetry, lookupPricing: pricing }, {
|
|
413
|
-
...baseRecord,
|
|
414
|
-
inputTokens: 0,
|
|
415
|
-
outputTokens: 0,
|
|
416
|
-
latencyMs: Math.max(0, authority.now() - startedAt),
|
|
417
|
-
status: input.abortSignal.aborted ? 'aborted' : 'error',
|
|
418
|
-
errorClass: evaluatorErrorClass(effectiveError),
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
catch (accountingError) {
|
|
422
|
-
throw new AuxiliaryModelCallLocalError('accounting', accountingError);
|
|
423
|
-
}
|
|
458
|
+
await recordUsageUnknown(input.abortSignal.aborted ? 'aborted' : 'failed', evaluatorErrorClass(effectiveError));
|
|
424
459
|
throw effectiveError;
|
|
425
460
|
}
|
|
426
461
|
try {
|
|
427
462
|
await recordLlmCallStrict({ repo: authority.telemetry, lookupPricing: pricing }, {
|
|
428
463
|
...baseRecord,
|
|
464
|
+
// A completion without reported usage stays on the legacy zero path:
|
|
465
|
+
// hosted execution settlement refuses any range containing
|
|
466
|
+
// usage-missing rows ('missing_attempt_usage'), so routing these
|
|
467
|
+
// rows to the canonical ledger marked every such hosted run
|
|
468
|
+
// indeterminate. The residual zero is known dishonesty, pending a
|
|
469
|
+
// ruling on that settlement coverage semantics (#5691).
|
|
429
470
|
...(result.usage
|
|
430
471
|
? llmCallUsageFields(result.usage)
|
|
431
472
|
: { inputTokens: 0, outputTokens: 0 }),
|
|
@@ -50,8 +50,10 @@ export class HostHostedExecutionRunner {
|
|
|
50
50
|
await (signal.aborted
|
|
51
51
|
? this.input.waitForAllResidencies()
|
|
52
52
|
: this.input.waitForExecutionResidencies());
|
|
53
|
-
const
|
|
54
|
-
const
|
|
53
|
+
const settledAt = (this.input.now ?? Date.now)();
|
|
54
|
+
const usage = await this.#readUsage(startedAt, settledAt);
|
|
55
|
+
const runCoverage = await this.input.runSettlementCoverage(startedAt, settledAt);
|
|
56
|
+
const incompleteUsage = incompleteUsageReason(usage, runCoverage);
|
|
55
57
|
if (incompleteUsage) {
|
|
56
58
|
return indeterminate(input.executionId, `Runtime Host usage did not settle: ${incompleteUsage}`);
|
|
57
59
|
}
|
|
@@ -111,15 +113,20 @@ export class HostHostedExecutionRunner {
|
|
|
111
113
|
return result;
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
|
-
function incompleteUsageReason(result) {
|
|
115
|
-
|
|
116
|
+
function incompleteUsageReason(result, runCoverage) {
|
|
117
|
+
// Unreadable and pending-repair rows stay ledger-wide: damage in the window
|
|
118
|
+
// is nobody's settled fact. Partial and missing usage are judged against the
|
|
119
|
+
// same window as the totals this check guards (#5890), so a delegated child
|
|
120
|
+
// Session cannot settle underneath them — the `no_run` sentinel rows stay
|
|
121
|
+
// excluded, and the shared coverage field still reports everything.
|
|
122
|
+
const { unreadableRecords, pendingRepairs } = result.provenance;
|
|
116
123
|
if (unreadableRecords > 0)
|
|
117
124
|
return 'unreadable_usage_record';
|
|
118
125
|
if (pendingRepairs > 0)
|
|
119
126
|
return 'pending_usage_repair';
|
|
120
|
-
if (
|
|
127
|
+
if (runCoverage.usagePartialAttempts > 0)
|
|
121
128
|
return 'partial_attempt_usage';
|
|
122
|
-
if (
|
|
129
|
+
if (runCoverage.usageMissingAttempts > 0)
|
|
123
130
|
return 'missing_attempt_usage';
|
|
124
131
|
return undefined;
|
|
125
132
|
}
|
|
@@ -33,6 +33,16 @@ export class HostUsagePricingCoordinator {
|
|
|
33
33
|
'pricing.query': (input) => this.#queryPricing(input),
|
|
34
34
|
'pricing.mutate': (input) => this.#mutatePricing(input),
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* What the settlement window itself left unsettled (#5890) — hosted
|
|
38
|
+
* execution settlement's incompleteness check reads the same window as the
|
|
39
|
+
* totals it guards, minus the `no_run` sentinel rows. Deliberately off the
|
|
40
|
+
* operation handler map: the ledger-wide `usage.query` coverage keeps a
|
|
41
|
+
* different shape, so settlement scopes itself through this in-process seam.
|
|
42
|
+
*/
|
|
43
|
+
async runSettlementCoverage(from, to) {
|
|
44
|
+
return this.#stores.modelCalls.modelCallRunSettlementCoverage(from, to);
|
|
45
|
+
}
|
|
36
46
|
#stores;
|
|
37
47
|
#requestDrain;
|
|
38
48
|
#activation;
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
import { decodeModelCallAttempt, MODEL_CALL_ATTEMPT_EVENT_TYPE, } from '@maka/core/model-call-attempt';
|
|
20
20
|
import { resolveUsageRange, } from '@maka/core/model-call-usage-projection';
|
|
21
21
|
import { usageBucketKey } from '@maka/core/usage-stats/bucket-key';
|
|
22
|
-
import { bucketGrouping, CACHE_READ_TOKENS, count, countableFilter, COVERAGE_SUMS, PRICED_COST, REQUEST_SUMS, TOKEN_SUMS, unreadableFilter, } from './model-call-usage-sql.js';
|
|
22
|
+
import { bucketGrouping, CACHE_READ_TOKENS, count, countableFilter, COVERAGE_SUMS, NO_RUN_TURN_ID, PRICED_COST, REQUEST_SUMS, RUN_SETTLEMENT_COVERAGE_SUMS, TOKEN_SUMS, unreadableFilter, } from './model-call-usage-sql.js';
|
|
23
23
|
import { acquireOperationalStateDatabase, } from './operational-state-store.js';
|
|
24
24
|
import { MODEL_CALL_COLUMNS } from './sqlite-usage-schema.js';
|
|
25
|
+
export { NO_RUN_TURN_ID };
|
|
25
26
|
export class ModelCallLedgerClosedError extends Error {
|
|
26
27
|
constructor() {
|
|
27
28
|
super('Model call ledger is draining or closed');
|
|
@@ -69,6 +70,11 @@ class SqliteModelCallLedger {
|
|
|
69
70
|
const eventsPerRun = positiveInteger(input.eventsPerRun, 512, 'projection catch-up event limit');
|
|
70
71
|
return this.write(() => catchUpModelCallProjection(this.#lease.database, input, limit, eventsPerRun));
|
|
71
72
|
}
|
|
73
|
+
recordUsageUnknownAttempt(record) {
|
|
74
|
+
if (this.#state !== 'open')
|
|
75
|
+
return Promise.reject(new ModelCallLedgerClosedError());
|
|
76
|
+
return this.write(() => writeUsageUnknownModelCallAttempt(this.#lease.database, record));
|
|
77
|
+
}
|
|
72
78
|
summary(query, now) {
|
|
73
79
|
const db = this.#open();
|
|
74
80
|
const range = resolveUsageRange(query.range, now);
|
|
@@ -159,6 +165,19 @@ class SqliteModelCallLedger {
|
|
|
159
165
|
unreadableRecords: this.#unreadable(query, range),
|
|
160
166
|
};
|
|
161
167
|
}
|
|
168
|
+
runSettlementCoverage(from, to) {
|
|
169
|
+
const db = this.#open();
|
|
170
|
+
const row = db
|
|
171
|
+
.prepare(`SELECT ${RUN_SETTLEMENT_COVERAGE_SUMS}
|
|
172
|
+
FROM usage_model_call_attempts
|
|
173
|
+
WHERE cost_basis IS NOT NULL AND no_run = 0
|
|
174
|
+
AND completed_at >= ? AND completed_at <= ?`)
|
|
175
|
+
.get(from, to);
|
|
176
|
+
return {
|
|
177
|
+
usageMissingAttempts: count(row?.usageMissingAttempts),
|
|
178
|
+
usagePartialAttempts: count(row?.usagePartialAttempts),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
162
181
|
#open() {
|
|
163
182
|
if (this.#state !== 'open')
|
|
164
183
|
throw new ModelCallLedgerClosedError();
|
|
@@ -285,12 +304,51 @@ function bindModelCallAttempt(attempt) {
|
|
|
285
304
|
reasoning_tokens: attempt.reasoningTokens ?? null,
|
|
286
305
|
cost_basis: attempt.costBasis,
|
|
287
306
|
cost_usd: attempt.costUsd ?? null,
|
|
307
|
+
// The projection's only source is the AgentRun authority: every row it
|
|
308
|
+
// writes is run-owned, whatever turn the run ran under.
|
|
309
|
+
no_run: 0,
|
|
288
310
|
};
|
|
289
311
|
return MODEL_CALL_COLUMNS.map((column) => values[column]);
|
|
290
312
|
}
|
|
291
313
|
function writeModelCallAttempt(db, attempt) {
|
|
292
314
|
db.prepare(MODEL_CALL_UPSERT).run(...bindModelCallAttempt(attempt));
|
|
293
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* The usage-unknown binding: no token counts and no cost exist to record, and
|
|
318
|
+
* the table's CHECKs refuse any row that claimed otherwise.
|
|
319
|
+
*/
|
|
320
|
+
function bindUsageUnknownModelCallAttempt(record) {
|
|
321
|
+
const values = {
|
|
322
|
+
attempt_id: record.attemptId,
|
|
323
|
+
completed_at: record.completedAt,
|
|
324
|
+
session_id: record.sessionId ?? null,
|
|
325
|
+
logical_call_id: record.logicalCallId,
|
|
326
|
+
turn_id: record.turnId,
|
|
327
|
+
call_kind: record.callKind,
|
|
328
|
+
connection_slug: record.connectionSlug ?? null,
|
|
329
|
+
provider_id: record.providerId,
|
|
330
|
+
model_id: record.modelId,
|
|
331
|
+
latency_ms: record.latencyMs,
|
|
332
|
+
status: record.status,
|
|
333
|
+
error_class: record.errorClass ?? null,
|
|
334
|
+
usage_basis: 'missing',
|
|
335
|
+
input_tokens: null,
|
|
336
|
+
output_tokens: null,
|
|
337
|
+
cache_read_input_tokens: null,
|
|
338
|
+
cache_miss_input_tokens: null,
|
|
339
|
+
cache_write_input_tokens: null,
|
|
340
|
+
reasoning_tokens: null,
|
|
341
|
+
cost_basis: 'unpriced',
|
|
342
|
+
cost_usd: null,
|
|
343
|
+
// The one write outside any run's event stream: the ledger marks the row
|
|
344
|
+
// no-run here rather than inferring ownership from the turn value.
|
|
345
|
+
no_run: 1,
|
|
346
|
+
};
|
|
347
|
+
return MODEL_CALL_COLUMNS.map((column) => values[column]);
|
|
348
|
+
}
|
|
349
|
+
function writeUsageUnknownModelCallAttempt(db, record) {
|
|
350
|
+
db.prepare(MODEL_CALL_UPSERT).run(...bindUsageUnknownModelCallAttempt(record));
|
|
351
|
+
}
|
|
294
352
|
function catchUpModelCallProjection(db, input, limit, eventsPerRun) {
|
|
295
353
|
const scope = projectionScope(input);
|
|
296
354
|
const lagging = db
|
|
@@ -36,6 +36,17 @@ export const CACHE_READ_TOKENS = `
|
|
|
36
36
|
export const PRICED_COST = `CASE WHEN cost_basis = 'priced' THEN COALESCE(cost_usd, 0) ELSE 0 END`;
|
|
37
37
|
/** Only a provider failure is an error. */
|
|
38
38
|
const ERROR_ROW = `status = 'failed'`;
|
|
39
|
+
/**
|
|
40
|
+
* The turn placeholder on rows recorded outside any AgentRun — failed and
|
|
41
|
+
* aborted auxiliary Host model calls (#5691). Such a row is honest accounting
|
|
42
|
+
* (real spend shape, unknown usage), so the ledger-wide coverage counts it.
|
|
43
|
+
* The value is a placeholder for the table's required turn column, not the
|
|
44
|
+
* settlement discriminator: rows carry a `no_run` column written by whichever
|
|
45
|
+
* seam recorded them, because a hosted execution legally named `auxiliary`
|
|
46
|
+
* owns rows under this same turn value (#5890 review). Shared with the writer
|
|
47
|
+
* so the placeholder cannot drift.
|
|
48
|
+
*/
|
|
49
|
+
export const NO_RUN_TURN_ID = 'auxiliary';
|
|
39
50
|
export const TOKEN_SUMS = `
|
|
40
51
|
SUM(COALESCE(input_tokens, 0)) AS input,
|
|
41
52
|
SUM(COALESCE(output_tokens, 0)) AS output,
|
|
@@ -51,6 +62,18 @@ export const COVERAGE_SUMS = `
|
|
|
51
62
|
SUM(usage_basis = 'reported') AS usageReportedAttempts,
|
|
52
63
|
SUM(usage_basis = 'partial') AS usagePartialAttempts,
|
|
53
64
|
SUM(usage_basis = 'missing') AS usageMissingAttempts`;
|
|
65
|
+
/**
|
|
66
|
+
* Coverage a hosted execution's settlement may hold a run to (#5691): the
|
|
67
|
+
* rows the run owns — the ones its Session's event-stream projection
|
|
68
|
+
* recorded, outside the `no_run` mark the usage-unknown seam writes. The
|
|
69
|
+
* ownership exclusion is the query's WHERE clause, not an aggregate here, so
|
|
70
|
+
* this stays narrower than the ledger-wide {@link COVERAGE_SUMS} on purpose:
|
|
71
|
+
* one answers "what does the ledger hold unknown-usage rows for", the other
|
|
72
|
+
* "what did this run leave unsettled".
|
|
73
|
+
*/
|
|
74
|
+
export const RUN_SETTLEMENT_COVERAGE_SUMS = `
|
|
75
|
+
SUM(usage_basis = 'partial') AS usagePartialAttempts,
|
|
76
|
+
SUM(usage_basis = 'missing') AS usageMissingAttempts`;
|
|
54
77
|
export const REQUEST_SUMS = `
|
|
55
78
|
COUNT(*) AS totalRequests,
|
|
56
79
|
SUM(${PRICED_COST}) AS totalCostUsd,
|
|
@@ -78,6 +101,21 @@ export function countableFilter(query, range) {
|
|
|
78
101
|
equals('provider_id', query.providerId);
|
|
79
102
|
equals('model_id', query.modelId);
|
|
80
103
|
equals('connection_slug', query.connectionSlug);
|
|
104
|
+
if (query.callKinds !== undefined) {
|
|
105
|
+
if (query.callKinds.length === 0) {
|
|
106
|
+
// An empty allowlist addresses no rows at all; SQL has no `IN ()`, so
|
|
107
|
+
// the honest translation is a clause that is never true.
|
|
108
|
+
clauses.push('0');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Rows written before call_kind was recorded (call_kind IS NULL) are
|
|
112
|
+
// legacy and cannot be classified: a callKinds filter excludes them on
|
|
113
|
+
// purpose, so pre-callKind Sessions report only their newer calls
|
|
114
|
+
// (#5691 review).
|
|
115
|
+
clauses.push(`call_kind IN (${query.callKinds.map(() => '?').join(', ')})`);
|
|
116
|
+
parameters.push(...query.callKinds);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
81
119
|
if (query.status !== undefined && query.status !== 'all') {
|
|
82
120
|
// `interrupted` joins `aborted`: both mean the call stopped short without
|
|
83
121
|
// the provider reporting a failure.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
export const SQLITE_USAGE_SCHEMA_VERSION =
|
|
19
|
+
export const SQLITE_USAGE_SCHEMA_VERSION = 10;
|
|
20
20
|
/**
|
|
21
21
|
* The canonical ledger's columns, in the order every statement binds them.
|
|
22
22
|
*
|
|
@@ -45,6 +45,7 @@ export const MODEL_CALL_COLUMNS = [
|
|
|
45
45
|
'reasoning_tokens',
|
|
46
46
|
'cost_basis',
|
|
47
47
|
'cost_usd',
|
|
48
|
+
'no_run',
|
|
48
49
|
];
|
|
49
50
|
/**
|
|
50
51
|
* The JSON path each pricing column was read from before the columns existed,
|
|
@@ -131,6 +132,11 @@ const MODEL_CALL_TABLE = `
|
|
|
131
132
|
reasoning_tokens INTEGER,
|
|
132
133
|
cost_basis TEXT,
|
|
133
134
|
cost_usd REAL,
|
|
135
|
+
-- Which writer recorded the row: the run's event-stream projection (0) or
|
|
136
|
+
-- the usage-unknown seam for calls no AgentRun owns (1). Settlement reads
|
|
137
|
+
-- this, not the turn value — a hosted execution legally named like the
|
|
138
|
+
-- no-run sentinel owns rows under the sentinel turn too (#5890 review).
|
|
139
|
+
no_run INTEGER NOT NULL DEFAULT 0,
|
|
134
140
|
CHECK (${MODEL_CALL_REQUIRED_COLUMNS.map((column) => `(${column} IS NULL) = (cost_basis IS NULL)`).join(' AND ')}),
|
|
135
141
|
-- A price that could not be resolved must never surface as an amount, and a
|
|
136
142
|
-- priced call must carry one. Zero stays legal: it is the only way to say a
|
|
@@ -209,6 +215,12 @@ export function migrateSqliteUsageDatabase(db) {
|
|
|
209
215
|
// through, and the conversion below reads one.
|
|
210
216
|
ensureColumn(db, 'usage_model_call_attempts', 'session_id', 'TEXT');
|
|
211
217
|
spreadModelCallRecordJson(db);
|
|
218
|
+
// no_run is written by the recorder, never inferred from the turn value:
|
|
219
|
+
// a backfill keyed on the auxiliary turn string would flip a legally named
|
|
220
|
+
// execution's own run-owned rows (see the settlement-coverage regression in
|
|
221
|
+
// model-call-usage-query.test.ts). Databases from before this column cannot
|
|
222
|
+
// hold auxiliary rows - recording them is what introduced the column.
|
|
223
|
+
ensureColumn(db, 'usage_model_call_attempts', 'no_run', 'INTEGER NOT NULL DEFAULT 0');
|
|
212
224
|
db.exec(`
|
|
213
225
|
CREATE INDEX IF NOT EXISTS usage_model_call_attempts_completed_at
|
|
214
226
|
ON usage_model_call_attempts(completed_at DESC, attempt_id);
|
|
@@ -350,7 +362,10 @@ function spreadModelCallRecordJson(db) {
|
|
|
350
362
|
attempt_id,
|
|
351
363
|
completed_at,
|
|
352
364
|
session_id,
|
|
353
|
-
${pricing}
|
|
365
|
+
${pricing},
|
|
366
|
+
-- Pre-column rows all predate the usage-unknown seam: every one of them
|
|
367
|
+
-- was recorded by the run's event-stream projection.
|
|
368
|
+
0 AS no_run
|
|
354
369
|
FROM classified;
|
|
355
370
|
|
|
356
371
|
DROP TABLE usage_model_call_attempts_blob;
|
|
@@ -232,6 +232,10 @@ class SqliteTelemetryRepo {
|
|
|
232
232
|
return false;
|
|
233
233
|
if (query.status && query.status !== 'all' && row.status !== query.status)
|
|
234
234
|
return false;
|
|
235
|
+
if (query.callKinds !== undefined &&
|
|
236
|
+
(row.callKind === undefined || !query.callKinds.includes(row.callKind))) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
235
239
|
return true;
|
|
236
240
|
});
|
|
237
241
|
}
|
|
@@ -309,7 +309,9 @@ function createWriterFacade(lease, telemetry, modelCalls, pricing, screen) {
|
|
|
309
309
|
modelCallSummary: (query, now) => read(() => modelCalls.summary(query, now)),
|
|
310
310
|
modelCallBuckets: (query, groupBy, now) => read(() => modelCalls.buckets(query, groupBy, now)),
|
|
311
311
|
modelCallLogs: (query, now, offset, limit) => read(() => modelCalls.logs(query, now, offset, limit)),
|
|
312
|
+
modelCallRunSettlementCoverage: (from, to) => read(() => modelCalls.runSettlementCoverage(from, to)),
|
|
312
313
|
catchUpModelCallProjection: admitModelCallProjectionCatchUp,
|
|
314
|
+
recordUsageUnknownAttempt: (record) => admitSessionUsageMutation(record.sessionId, () => modelCalls.recordUsageUnknownAttempt(record)),
|
|
313
315
|
},
|
|
314
316
|
pricing: {
|
|
315
317
|
snapshot: () => read(() => pricing.snapshot()),
|
|
@@ -343,6 +345,7 @@ function modelCallReader(ledger, run) {
|
|
|
343
345
|
modelCallSummary: (query, now) => run(() => ledger.summary(query, now)),
|
|
344
346
|
modelCallBuckets: (query, groupBy, now) => run(() => ledger.buckets(query, groupBy, now)),
|
|
345
347
|
modelCallLogs: (query, now, offset, limit) => run(() => ledger.logs(query, now, offset, limit)),
|
|
348
|
+
modelCallRunSettlementCoverage: (from, to) => run(() => ledger.runSettlementCoverage(from, to)),
|
|
346
349
|
});
|
|
347
350
|
}
|
|
348
351
|
function pricingReader(store, run) {
|