@sixb/sqlite 0.1.25 → 0.2.0
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/dist/agents/agent-storage.d.ts +8 -3
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/computers.d.ts +22 -0
- package/dist/agents/computers.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +6 -0
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +2 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts +4 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/agents/token-rates.d.ts +13 -0
- package/dist/agents/token-rates.d.ts.map +1 -0
- package/dist/ai-accounting-filters.d.ts +14 -0
- package/dist/ai-accounting-filters.d.ts.map +1 -0
- package/dist/ai-cost-groups.d.ts +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/index.js +465 -63
- package/dist/index.js.map +17 -14
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/ontology-storage/commits.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +2 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +10 -3
- package/src/agents/computers.ts +112 -0
- package/src/agents/messages.ts +13 -0
- package/src/agents/rows.ts +14 -0
- package/src/agents/runs.ts +52 -4
- package/src/agents/threads.ts +108 -4
- package/src/agents/token-rates.ts +90 -0
- package/src/ai-accounting-filters.ts +51 -0
- package/src/ai-cost-groups.ts +52 -14
- package/src/ai-cost-storage.ts +101 -39
- package/src/migrations/080-agent-run-cancel-requests.sql +3 -0
- package/src/migrations/081-agent-thread-runs-on.sql +4 -0
- package/src/migrations/082-agent-token-rates.sql +17 -0
- package/src/migrations/083-agent-computer-sessions.sql +27 -0
- package/src/migrations.ts +12 -0
- package/src/objects/query-compiler.ts +44 -28
- package/src/ontology-storage/commits.ts +9 -0
- package/src/ontology-storage/materializations.ts +18 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
AGENT_TOKEN_RATE_SAMPLES,
|
|
4
|
+
assertAgentTokenRateRecord,
|
|
5
|
+
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
6
|
+
import type { AgentTokenRateRecord, AgentTokenRateStore } from "@sixb/core/storage"
|
|
7
|
+
import { runImmediateTransaction } from "../transactions"
|
|
8
|
+
|
|
9
|
+
interface TokenRateRow {
|
|
10
|
+
readonly project_id: string
|
|
11
|
+
readonly run_id: string
|
|
12
|
+
readonly provider_id: string
|
|
13
|
+
readonly model_id: string
|
|
14
|
+
readonly bytes: number
|
|
15
|
+
readonly tokens: number
|
|
16
|
+
readonly measured_at: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SqliteAgentTokenRateStore implements AgentTokenRateStore {
|
|
20
|
+
constructor(private readonly db: Database) {}
|
|
21
|
+
|
|
22
|
+
async record(input: AgentTokenRateRecord): Promise<void> {
|
|
23
|
+
assertAgentTokenRateRecord(input, "SixbSqlite")
|
|
24
|
+
|
|
25
|
+
runImmediateTransaction(this.db, () => {
|
|
26
|
+
this.db
|
|
27
|
+
.query(
|
|
28
|
+
`INSERT INTO agent_token_rates (
|
|
29
|
+
project_id, run_id, provider_id, model_id, bytes, tokens, measured_at
|
|
30
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
31
|
+
ON CONFLICT (project_id, run_id) DO UPDATE SET
|
|
32
|
+
provider_id = excluded.provider_id, model_id = excluded.model_id,
|
|
33
|
+
bytes = excluded.bytes, tokens = excluded.tokens, measured_at = excluded.measured_at`
|
|
34
|
+
)
|
|
35
|
+
.run(
|
|
36
|
+
input.projectId,
|
|
37
|
+
input.runId,
|
|
38
|
+
input.providerId,
|
|
39
|
+
input.modelId,
|
|
40
|
+
input.bytes,
|
|
41
|
+
input.tokens,
|
|
42
|
+
input.measuredAt.toISOString()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
this.db
|
|
46
|
+
.query(
|
|
47
|
+
`DELETE FROM agent_token_rates
|
|
48
|
+
WHERE project_id = ?1 AND provider_id = ?2 AND model_id = ?3
|
|
49
|
+
AND run_id NOT IN (
|
|
50
|
+
SELECT run_id FROM agent_token_rates
|
|
51
|
+
WHERE project_id = ?1 AND provider_id = ?2 AND model_id = ?3
|
|
52
|
+
ORDER BY measured_at DESC, run_id DESC
|
|
53
|
+
LIMIT ?4
|
|
54
|
+
)`
|
|
55
|
+
)
|
|
56
|
+
.run(input.projectId, input.providerId, input.modelId, AGENT_TOKEN_RATE_SAMPLES)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async listLatest(input: {
|
|
61
|
+
readonly projectId: string
|
|
62
|
+
readonly providerId: string
|
|
63
|
+
readonly modelId: string
|
|
64
|
+
}): Promise<readonly AgentTokenRateRecord[]> {
|
|
65
|
+
const rows = this.db
|
|
66
|
+
.query(
|
|
67
|
+
`SELECT project_id, run_id, provider_id, model_id, bytes, tokens, measured_at
|
|
68
|
+
FROM agent_token_rates
|
|
69
|
+
WHERE project_id = ? AND provider_id = ? AND model_id = ?
|
|
70
|
+
ORDER BY measured_at DESC, run_id DESC
|
|
71
|
+
LIMIT ?`
|
|
72
|
+
)
|
|
73
|
+
.all(
|
|
74
|
+
input.projectId,
|
|
75
|
+
input.providerId,
|
|
76
|
+
input.modelId,
|
|
77
|
+
AGENT_TOKEN_RATE_SAMPLES
|
|
78
|
+
) as TokenRateRow[]
|
|
79
|
+
|
|
80
|
+
return rows.map((row) => ({
|
|
81
|
+
projectId: row.project_id,
|
|
82
|
+
providerId: row.provider_id,
|
|
83
|
+
modelId: row.model_id,
|
|
84
|
+
runId: row.run_id,
|
|
85
|
+
bytes: row.bytes,
|
|
86
|
+
tokens: row.tokens,
|
|
87
|
+
measuredAt: new Date(row.measured_at),
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { normalizeAiAccountingQuery } from "@sixb/core/internal/ai-cost-storage-provider"
|
|
2
|
+
|
|
3
|
+
// The kind, source and requester filters every accounting read shares, over the joins it makes:
|
|
4
|
+
// `execution`, `direct_agent`, `workflow_node`.
|
|
5
|
+
|
|
6
|
+
/** Where a call came from, as `aiAccountingSource` reads its attribution. */
|
|
7
|
+
export const SQLITE_SOURCE_WORKFLOW = `COALESCE(
|
|
8
|
+
workflow_node.workflow_id,
|
|
9
|
+
CASE WHEN execution.executor_kind = 'workflow' THEN execution.authority_primitive_id END
|
|
10
|
+
)`
|
|
11
|
+
export const SQLITE_SOURCE_KIND = `CASE
|
|
12
|
+
WHEN direct_agent.id IS NOT NULL THEN 'agent'
|
|
13
|
+
WHEN ${SQLITE_SOURCE_WORKFLOW} IS NOT NULL THEN 'workflow'
|
|
14
|
+
WHEN execution.executor_kind = 'action' THEN 'action'
|
|
15
|
+
ELSE 'other'
|
|
16
|
+
END`
|
|
17
|
+
|
|
18
|
+
/** The kind, source and requester filters over the columns that hold them; 12 positional params. */
|
|
19
|
+
export function sqliteAccountingFilter(columns: {
|
|
20
|
+
readonly kind: string
|
|
21
|
+
readonly sourceKind: string
|
|
22
|
+
readonly sourceWorkflow: string
|
|
23
|
+
readonly userId: string
|
|
24
|
+
readonly serviceAccountId: string
|
|
25
|
+
}): string {
|
|
26
|
+
return `(? IS NULL OR ${columns.kind} = ?)
|
|
27
|
+
AND (? IS NULL OR ${columns.sourceKind} = ?)
|
|
28
|
+
AND (? IS NULL OR ${columns.sourceWorkflow} = ?)
|
|
29
|
+
AND (
|
|
30
|
+
? IS NULL
|
|
31
|
+
OR (? = 'none' AND ${columns.userId} IS NULL AND ${columns.serviceAccountId} IS NULL)
|
|
32
|
+
OR (? = 'user' AND ${columns.userId} = ?)
|
|
33
|
+
OR (? = 'serviceAccount' AND ${columns.serviceAccountId} = ?)
|
|
34
|
+
)`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sqliteAccountingFilterParameters(
|
|
38
|
+
query: ReturnType<typeof normalizeAiAccountingQuery>
|
|
39
|
+
): readonly (string | null)[] {
|
|
40
|
+
const kind = query.modelKind ?? null
|
|
41
|
+
const source = query.source?.kind ?? null
|
|
42
|
+
const workflow = query.source?.kind === "workflow" ? (query.source.workflowId ?? null) : null
|
|
43
|
+
const mode =
|
|
44
|
+
query.requester === undefined
|
|
45
|
+
? null
|
|
46
|
+
: query.requester === "none"
|
|
47
|
+
? "none"
|
|
48
|
+
: query.requester.type
|
|
49
|
+
const id = query.requester === undefined || query.requester === "none" ? null : query.requester.id
|
|
50
|
+
return [kind, kind, source, source, workflow, workflow, mode, mode, mode, id, mode, id]
|
|
51
|
+
}
|
package/src/ai-cost-groups.ts
CHANGED
|
@@ -3,17 +3,31 @@ import {
|
|
|
3
3
|
aiModelCallGroupFragmentFromRow,
|
|
4
4
|
buildAiModelCallGroupsFromFragments,
|
|
5
5
|
normalizeAiModelCallAccountingQuery,
|
|
6
|
+
normalizeAiModelCallGroupSort,
|
|
6
7
|
TOKENLESS_MODEL_KINDS,
|
|
7
8
|
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
8
9
|
import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
|
|
10
|
+
import {
|
|
11
|
+
SQLITE_SOURCE_KIND,
|
|
12
|
+
SQLITE_SOURCE_WORKFLOW,
|
|
13
|
+
sqliteAccountingFilter,
|
|
14
|
+
sqliteAccountingFilterParameters,
|
|
15
|
+
} from "./ai-accounting-filters"
|
|
9
16
|
import type { SqliteStoreConnection } from "./transactions"
|
|
10
17
|
|
|
11
|
-
/**
|
|
18
|
+
/** Positional `?` placeholders numbered from `start`, to sit among this file's numbered ones. */
|
|
19
|
+
function numbered(sql: string, start: number): string {
|
|
20
|
+
let next = start
|
|
21
|
+
return sql.replace(/\?(?!\d)/g, () => `?${next++}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Paginate conversations, workflow runs and other initiating executions, then aggregate their matching calls without loading usage payloads. */
|
|
12
25
|
export async function listAiModelCallGroups(
|
|
13
26
|
connection: SqliteStoreConnection,
|
|
14
27
|
input: ListAiModelCallGroupsInput
|
|
15
28
|
): Promise<ListAiModelCallGroupsResult> {
|
|
16
29
|
const query = normalizeAiModelCallAccountingQuery(input)
|
|
30
|
+
const sort = normalizeAiModelCallGroupSort(input.sort)
|
|
17
31
|
const parameters = [
|
|
18
32
|
query.projectId,
|
|
19
33
|
query.from.toISOString(),
|
|
@@ -22,14 +36,15 @@ export async function listAiModelCallGroups(
|
|
|
22
36
|
query.modelId ?? null,
|
|
23
37
|
query.valuationStatus ?? null,
|
|
24
38
|
]
|
|
25
|
-
const
|
|
39
|
+
const filters = sqliteAccountingFilterParameters(query)
|
|
40
|
+
const count = connection.db.query(COUNT_SQL).get(...parameters, ...filters) as { total: string }
|
|
26
41
|
const rows = connection.db
|
|
27
42
|
.query(PAGE_SQL)
|
|
28
|
-
.all(...parameters, query.limit, query.offset) as AiModelCallGroupRow[]
|
|
43
|
+
.all(...parameters, ...filters, query.limit, query.offset, sort) as AiModelCallGroupRow[]
|
|
29
44
|
const total = Number(count?.total)
|
|
30
45
|
if (!Number.isSafeInteger(total) || total < 0)
|
|
31
46
|
throw new RangeError("[Sixb] Invalid model-call group count.")
|
|
32
|
-
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow))
|
|
47
|
+
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow), sort)
|
|
33
48
|
return { items, total, hasMore: query.offset + items.length < total }
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -39,8 +54,15 @@ const TOKENLESS_KINDS = TOKENLESS_MODEL_KINDS.map((kind) => `'${kind}'`).join(",
|
|
|
39
54
|
const FILTERED_SQL = `
|
|
40
55
|
WITH filtered AS (
|
|
41
56
|
SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
|
|
57
|
+
-- What the call is listed under, as aiModelCallGroupKey computes it.
|
|
58
|
+
CASE
|
|
59
|
+
WHEN root_agent.thread_id IS NOT NULL THEN 'thread:' || root_agent.thread_id
|
|
60
|
+
WHEN workflow_node.workflow_run_id IS NOT NULL THEN 'workflowRun:' || workflow_node.workflow_run_id
|
|
61
|
+
ELSE COALESCE(root_agent.execution_id, usage.execution_id)
|
|
62
|
+
END AS group_key,
|
|
42
63
|
usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
|
|
43
|
-
usage.total_tokens, usage.
|
|
64
|
+
usage.total_tokens, usage.input_tokens, usage.cache_read_input_tokens,
|
|
65
|
+
usage.model_kind, usage.output_images, usage.input_characters,
|
|
44
66
|
usage.audio_duration_ms,
|
|
45
67
|
execution.executor_kind, execution.executor_id, execution.authority_primitive_id AS primitive_id,
|
|
46
68
|
COALESCE(cost.status, 'unvalued') AS valuation_status,
|
|
@@ -53,7 +75,8 @@ WITH filtered AS (
|
|
|
53
75
|
workflow_node.node_id AS workflow_agent_step_id,
|
|
54
76
|
workflow_agent.node_run_id AS workflow_node_run_id,
|
|
55
77
|
workflow_node.workflow_id AS workflow_id,
|
|
56
|
-
workflow_node.workflow_run_id AS workflow_run_id
|
|
78
|
+
workflow_node.workflow_run_id AS workflow_run_id,
|
|
79
|
+
execution.requested_by_user_id, execution.requested_by_service_account_id
|
|
57
80
|
FROM ai_model_call_usage AS usage
|
|
58
81
|
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
59
82
|
LEFT JOIN agent_runs AS direct_agent
|
|
@@ -73,23 +96,36 @@ WITH filtered AS (
|
|
|
73
96
|
AND (?4 IS NULL OR usage.provider_id = ?4)
|
|
74
97
|
AND (?5 IS NULL OR usage.requested_model_id = ?5)
|
|
75
98
|
AND (?6 IS NULL OR COALESCE(cost.status, 'unvalued') = ?6)
|
|
99
|
+
AND ${numbered(
|
|
100
|
+
sqliteAccountingFilter({
|
|
101
|
+
kind: "usage.model_kind",
|
|
102
|
+
sourceKind: SQLITE_SOURCE_KIND,
|
|
103
|
+
sourceWorkflow: SQLITE_SOURCE_WORKFLOW,
|
|
104
|
+
userId: "execution.requested_by_user_id",
|
|
105
|
+
serviceAccountId: "execution.requested_by_service_account_id",
|
|
106
|
+
}),
|
|
107
|
+
7
|
|
108
|
+
)}
|
|
76
109
|
), roots AS (
|
|
77
|
-
SELECT
|
|
78
|
-
FROM filtered GROUP BY
|
|
110
|
+
SELECT group_key, MAX(occurred_at) AS last_call_at, TOTAL(amount_nanos) AS cost_nanos
|
|
111
|
+
FROM filtered GROUP BY group_key
|
|
79
112
|
)
|
|
80
113
|
`
|
|
81
114
|
const COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`
|
|
82
115
|
const PAGE_SQL = `${FILTERED_SQL}, page AS (
|
|
83
|
-
SELECT
|
|
84
|
-
ORDER BY last_call_at DESC,
|
|
116
|
+
SELECT group_key FROM roots
|
|
117
|
+
ORDER BY CASE WHEN ?21 = 'cost' THEN cost_nanos END DESC, last_call_at DESC, group_key ASC
|
|
118
|
+
LIMIT ?19 OFFSET ?20
|
|
85
119
|
)
|
|
86
|
-
SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
120
|
+
SELECT filtered.group_key, root_execution_id, execution_id, provider_id, requested_model_id,
|
|
87
121
|
MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
|
|
88
122
|
CAST(COUNT(*) AS TEXT) AS call_count,
|
|
89
123
|
CAST(COUNT(total_tokens) AS TEXT) AS token_count,
|
|
90
124
|
CAST(COUNT(CASE WHEN total_tokens IS NULL AND model_kind IN (${TOKENLESS_KINDS}) THEN 1 END) AS TEXT)
|
|
91
125
|
AS tokenless_count,
|
|
92
126
|
CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
|
|
127
|
+
CAST(SUM(input_tokens) AS TEXT) AS input_tokens,
|
|
128
|
+
CAST(SUM(cache_read_input_tokens) AS TEXT) AS cache_read_input_tokens,
|
|
93
129
|
CAST(SUM(output_images) AS TEXT) AS output_images,
|
|
94
130
|
CAST(SUM(input_characters) AS TEXT) AS input_characters,
|
|
95
131
|
CAST(SUM(audio_duration_ms) AS TEXT) AS audio_duration_ms,
|
|
@@ -106,8 +142,10 @@ SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id
|
|
|
106
142
|
MAX(workflow_agent_step_id) AS workflow_agent_step_id,
|
|
107
143
|
MAX(workflow_node_run_id) AS workflow_node_run_id,
|
|
108
144
|
MAX(workflow_id) AS workflow_id,
|
|
109
|
-
MAX(workflow_run_id) AS workflow_run_id
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
MAX(workflow_run_id) AS workflow_run_id,
|
|
146
|
+
MAX(requested_by_user_id) AS requested_by_user_id,
|
|
147
|
+
MAX(requested_by_service_account_id) AS requested_by_service_account_id
|
|
148
|
+
FROM filtered JOIN page ON page.group_key = filtered.group_key
|
|
149
|
+
GROUP BY filtered.group_key, root_execution_id, execution_id, provider_id, requested_model_id,
|
|
112
150
|
valuation_status, amount_currency
|
|
113
151
|
`
|
package/src/ai-cost-storage.ts
CHANGED
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
type QueryAiAccountingOverviewInput,
|
|
29
29
|
type SummarizeAiCostExecutionsInput,
|
|
30
30
|
} from "@sixb/core/storage"
|
|
31
|
+
import {
|
|
32
|
+
SQLITE_SOURCE_KIND,
|
|
33
|
+
SQLITE_SOURCE_WORKFLOW,
|
|
34
|
+
sqliteAccountingFilter,
|
|
35
|
+
sqliteAccountingFilterParameters,
|
|
36
|
+
} from "./ai-accounting-filters"
|
|
31
37
|
import { listAiModelCallGroups } from "./ai-cost-groups"
|
|
32
38
|
import { requestedReasoningFromRow } from "./ai-usage-storage"
|
|
33
39
|
import { isUniqueConstraintError } from "./storage-errors"
|
|
@@ -96,6 +102,8 @@ export class SqliteAiCostStorage implements AiCostStorage {
|
|
|
96
102
|
query.providerId ?? null,
|
|
97
103
|
query.modelId ?? null,
|
|
98
104
|
query.modelId ?? null,
|
|
105
|
+
...sqliteAccountingFilterParameters(query),
|
|
106
|
+
input.bucket,
|
|
99
107
|
input.bucket
|
|
100
108
|
) as AggregateRow[]
|
|
101
109
|
return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow))
|
|
@@ -176,13 +184,34 @@ const SQLITE_ACCOUNTING_LIST_FILTER = `
|
|
|
176
184
|
ELSE 'unvalued'
|
|
177
185
|
END
|
|
178
186
|
)
|
|
187
|
+
AND ${sqliteAccountingFilter({
|
|
188
|
+
kind: "usage.model_kind",
|
|
189
|
+
sourceKind: SQLITE_SOURCE_KIND,
|
|
190
|
+
sourceWorkflow: SQLITE_SOURCE_WORKFLOW,
|
|
191
|
+
userId: "execution.requested_by_user_id",
|
|
192
|
+
serviceAccountId: "execution.requested_by_service_account_id",
|
|
193
|
+
})}
|
|
194
|
+
`
|
|
195
|
+
|
|
196
|
+
const SQLITE_ACCOUNTING_LIST_JOINS = `
|
|
197
|
+
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
198
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
199
|
+
ON direct_agent.project_id = usage.project_id
|
|
200
|
+
AND direct_agent.execution_id = usage.execution_id
|
|
201
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
202
|
+
ON workflow_agent.project_id = usage.project_id
|
|
203
|
+
AND workflow_agent.execution_id = usage.execution_id
|
|
204
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
205
|
+
ON workflow_node.project_id = workflow_agent.project_id
|
|
206
|
+
AND workflow_node.id = workflow_agent.node_run_id
|
|
207
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
208
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
179
209
|
`
|
|
180
210
|
|
|
181
211
|
const SQLITE_ACCOUNTING_LIST_COUNT_SQL = `
|
|
182
212
|
SELECT CAST(COUNT(*) AS TEXT) AS total
|
|
183
213
|
FROM ai_model_call_usage AS usage
|
|
184
|
-
|
|
185
|
-
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
214
|
+
${SQLITE_ACCOUNTING_LIST_JOINS}
|
|
186
215
|
WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
|
|
187
216
|
`
|
|
188
217
|
|
|
@@ -218,18 +247,7 @@ const SQLITE_ACCOUNTING_LIST_PAGE_SQL = `
|
|
|
218
247
|
cost.details AS cost_details,
|
|
219
248
|
cost.rated_at AS cost_rated_at
|
|
220
249
|
FROM ai_model_call_usage AS usage
|
|
221
|
-
|
|
222
|
-
LEFT JOIN agent_runs AS direct_agent
|
|
223
|
-
ON direct_agent.project_id = usage.project_id
|
|
224
|
-
AND direct_agent.execution_id = usage.execution_id
|
|
225
|
-
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
226
|
-
ON workflow_agent.project_id = usage.project_id
|
|
227
|
-
AND workflow_agent.execution_id = usage.execution_id
|
|
228
|
-
LEFT JOIN workflow_node_runs AS workflow_node
|
|
229
|
-
ON workflow_node.project_id = workflow_agent.project_id
|
|
230
|
-
AND workflow_node.id = workflow_agent.node_run_id
|
|
231
|
-
LEFT JOIN ai_model_call_valuations AS cost
|
|
232
|
-
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
250
|
+
${SQLITE_ACCOUNTING_LIST_JOINS}
|
|
233
251
|
WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
|
|
234
252
|
ORDER BY usage.occurred_at DESC, usage.id ASC
|
|
235
253
|
LIMIT ? OFFSET ?
|
|
@@ -269,7 +287,10 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
269
287
|
ELSE 'unvalued'
|
|
270
288
|
END AS valuation_status,
|
|
271
289
|
CASE WHEN cost.status = 'rated' THEN cost.currency ELSE NULL END AS amount_currency,
|
|
272
|
-
CASE WHEN cost.status = 'rated' THEN cost.amount_nanos ELSE NULL END AS amount_nanos
|
|
290
|
+
CASE WHEN cost.status = 'rated' THEN cost.amount_nanos ELSE NULL END AS amount_nanos,
|
|
291
|
+
execution.requested_by_user_id AS requester_user_id,
|
|
292
|
+
execution.requested_by_service_account_id AS requester_service_account_id,
|
|
293
|
+
${SQLITE_SOURCE_KIND} AS source_kind
|
|
273
294
|
FROM ai_model_call_usage AS usage
|
|
274
295
|
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
275
296
|
LEFT JOIN agent_runs AS direct_agent
|
|
@@ -286,10 +307,19 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
286
307
|
WHERE usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
|
|
287
308
|
AND (? IS NULL OR usage.provider_id = ?)
|
|
288
309
|
AND (? IS NULL OR usage.requested_model_id = ?)
|
|
310
|
+
), filtered AS (
|
|
311
|
+
SELECT * FROM classified
|
|
312
|
+
WHERE ${sqliteAccountingFilter({
|
|
313
|
+
kind: "model_kind",
|
|
314
|
+
sourceKind: "source_kind",
|
|
315
|
+
sourceWorkflow: "attribution_workflow_id",
|
|
316
|
+
userId: "requester_user_id",
|
|
317
|
+
serviceAccountId: "requester_service_account_id",
|
|
318
|
+
})}
|
|
289
319
|
), expanded AS (
|
|
290
320
|
SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS key_3, NULL AS bucket_start,
|
|
291
|
-
|
|
292
|
-
FROM
|
|
321
|
+
filtered.*
|
|
322
|
+
FROM filtered
|
|
293
323
|
UNION ALL
|
|
294
324
|
SELECT 'series', NULL, NULL, NULL,
|
|
295
325
|
CASE ?
|
|
@@ -301,18 +331,39 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
301
331
|
printf('-%d days', (CAST(strftime('%w', occurred_at) AS INTEGER) + 6) % 7)
|
|
302
332
|
)
|
|
303
333
|
END,
|
|
304
|
-
|
|
305
|
-
FROM
|
|
334
|
+
filtered.*
|
|
335
|
+
FROM filtered
|
|
306
336
|
UNION ALL
|
|
307
|
-
SELECT '
|
|
337
|
+
SELECT 'modelSeries', provider_id, requested_model_id, NULL,
|
|
338
|
+
CASE ?
|
|
339
|
+
WHEN 'hour' THEN strftime('%Y-%m-%dT%H:00:00.000Z', occurred_at)
|
|
340
|
+
WHEN 'day' THEN strftime('%Y-%m-%dT00:00:00.000Z', occurred_at)
|
|
341
|
+
ELSE strftime(
|
|
342
|
+
'%Y-%m-%dT00:00:00.000Z',
|
|
343
|
+
occurred_at,
|
|
344
|
+
printf('-%d days', (CAST(strftime('%w', occurred_at) AS INTEGER) + 6) % 7)
|
|
345
|
+
)
|
|
346
|
+
END,
|
|
347
|
+
filtered.*
|
|
348
|
+
FROM filtered
|
|
308
349
|
UNION ALL
|
|
309
|
-
SELECT '
|
|
350
|
+
SELECT 'model', provider_id, requested_model_id, NULL, NULL, filtered.* FROM filtered
|
|
351
|
+
UNION ALL
|
|
352
|
+
SELECT 'person',
|
|
353
|
+
CASE
|
|
354
|
+
WHEN requester_user_id IS NOT NULL THEN 'user'
|
|
355
|
+
WHEN requester_service_account_id IS NOT NULL THEN 'serviceAccount'
|
|
356
|
+
END,
|
|
357
|
+
COALESCE(requester_user_id, requester_service_account_id), NULL, NULL, filtered.*
|
|
358
|
+
FROM filtered
|
|
310
359
|
UNION ALL
|
|
311
|
-
SELECT '
|
|
312
|
-
FROM classified WHERE attribution_agent_kind IS NOT NULL
|
|
360
|
+
SELECT 'kind', model_kind, NULL, NULL, NULL, filtered.* FROM filtered
|
|
313
361
|
UNION ALL
|
|
314
|
-
SELECT '
|
|
315
|
-
FROM
|
|
362
|
+
SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, filtered.*
|
|
363
|
+
FROM filtered WHERE attribution_agent_kind IS NOT NULL
|
|
364
|
+
UNION ALL
|
|
365
|
+
SELECT 'workflow', attribution_workflow_id, NULL, NULL, NULL, filtered.*
|
|
366
|
+
FROM filtered WHERE attribution_workflow_id IS NOT NULL
|
|
316
367
|
)
|
|
317
368
|
SELECT dimension,
|
|
318
369
|
key_1,
|
|
@@ -377,6 +428,7 @@ function accountingListFilterParameters(
|
|
|
377
428
|
query.executionId ?? null,
|
|
378
429
|
query.valuationStatus ?? null,
|
|
379
430
|
query.valuationStatus ?? null,
|
|
431
|
+
...sqliteAccountingFilterParameters(query),
|
|
380
432
|
]
|
|
381
433
|
}
|
|
382
434
|
|
|
@@ -505,20 +557,30 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
505
557
|
const dimensionKeys =
|
|
506
558
|
row.dimension === "model"
|
|
507
559
|
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
508
|
-
: row.dimension === "
|
|
509
|
-
?
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
560
|
+
: row.dimension === "modelSeries"
|
|
561
|
+
? {
|
|
562
|
+
providerId: required(row.key_1),
|
|
563
|
+
modelId: required(row.key_2),
|
|
564
|
+
start: new Date(required(row.bucket_start)),
|
|
565
|
+
}
|
|
566
|
+
: row.dimension === "person"
|
|
567
|
+
? row.key_1 && row.key_2
|
|
568
|
+
? { requesterType: row.key_1 as "user" | "serviceAccount", requesterId: row.key_2 }
|
|
569
|
+
: {}
|
|
570
|
+
: row.dimension === "kind"
|
|
571
|
+
? withModelKind(aiAccountingModelKind(row.key_1))
|
|
572
|
+
: row.dimension === "agent"
|
|
573
|
+
? {
|
|
574
|
+
agentKind: required(row.key_1) as "agent" | "workflowAgent",
|
|
575
|
+
...(row.key_1 === "workflowAgent"
|
|
576
|
+
? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
|
|
577
|
+
: {}),
|
|
578
|
+
}
|
|
579
|
+
: row.dimension === "workflow"
|
|
580
|
+
? { workflowId: required(row.key_1) }
|
|
581
|
+
: row.dimension === "series"
|
|
582
|
+
? { start: new Date(required(row.bucket_start)) }
|
|
583
|
+
: {}
|
|
522
584
|
return {
|
|
523
585
|
dimension: row.dimension,
|
|
524
586
|
...dimensionKeys,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Where a conversation's turns run: NULL in the cloud, 'computer' on its owner's computer.
|
|
2
|
+
ALTER TABLE agent_threads ADD COLUMN runs_on TEXT CHECK (runs_on IS NULL OR runs_on = 'computer');
|
|
3
|
+
-- Which of its owner's computers runs a computer's thread: NULL until one claims its first turn.
|
|
4
|
+
ALTER TABLE agent_threads ADD COLUMN computer_id TEXT;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- What each model's tokenizer measured on a project's agent runs, one row per run: the bytes of a
|
|
2
|
+
-- request's text as the Agent's context estimator weighs them, against the text tokens the
|
|
3
|
+
-- provider counted. A record deletes the model's rows past its newest 20, so the table stays small.
|
|
4
|
+
-- Timestamps are ISO-8601 UTC text and compare lexicographically.
|
|
5
|
+
CREATE TABLE agent_token_rates (
|
|
6
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
7
|
+
run_id TEXT NOT NULL CHECK (length(trim(run_id)) > 0),
|
|
8
|
+
provider_id TEXT NOT NULL CHECK (length(trim(provider_id)) > 0),
|
|
9
|
+
model_id TEXT NOT NULL CHECK (length(trim(model_id)) > 0),
|
|
10
|
+
bytes INTEGER NOT NULL CHECK (bytes > 0),
|
|
11
|
+
tokens INTEGER NOT NULL CHECK (tokens > 0),
|
|
12
|
+
measured_at TEXT NOT NULL,
|
|
13
|
+
PRIMARY KEY (project_id, run_id)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX idx_agent_token_rates_model
|
|
17
|
+
ON agent_token_rates (project_id, provider_id, model_id, measured_at DESC, run_id DESC);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Where a computer's thread runs: the computer's name and the folder there, beside its id. A
|
|
2
|
+
-- computer kept before folders were has none until its owner moves the thread.
|
|
3
|
+
ALTER TABLE agent_threads ADD COLUMN computer_name TEXT;
|
|
4
|
+
ALTER TABLE agent_threads ADD COLUMN computer_folder TEXT;
|
|
5
|
+
UPDATE agent_threads SET computer_id = NULL WHERE computer_id IS NOT NULL;
|
|
6
|
+
-- When its turns last moved to where they run now: the files earlier turns used are not there.
|
|
7
|
+
ALTER TABLE agent_threads ADD COLUMN moved_at TEXT;
|
|
8
|
+
|
|
9
|
+
-- Each open sixb on its owner's computers, a lease it renews: while one is live, the threads that
|
|
10
|
+
-- run on its computer in its folder are reachable there.
|
|
11
|
+
CREATE TABLE agent_computer_sessions (
|
|
12
|
+
project_id TEXT NOT NULL,
|
|
13
|
+
owner_principal_type TEXT NOT NULL CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
14
|
+
owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
|
|
15
|
+
id TEXT NOT NULL CHECK (length(id) > 0),
|
|
16
|
+
computer_id TEXT NOT NULL,
|
|
17
|
+
computer_name TEXT NOT NULL,
|
|
18
|
+
folder TEXT NOT NULL,
|
|
19
|
+
expires_at TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY (project_id, owner_principal_type, owner_principal_id, id)
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX agent_computer_sessions_by_expiry ON agent_computer_sessions (project_id, expires_at);
|
|
23
|
+
|
|
24
|
+
-- Each open sixb asks every 20 seconds which of its owner's threads here have a turn active.
|
|
25
|
+
CREATE INDEX agent_threads_active_on_computer
|
|
26
|
+
ON agent_threads (project_id, owner_principal_type, owner_principal_id, computer_id, computer_folder)
|
|
27
|
+
WHERE active_run_id IS NOT NULL;
|
package/src/migrations.ts
CHANGED
|
@@ -311,6 +311,10 @@ export const sqliteStorageMigrations = defineMigrations({
|
|
|
311
311
|
{ checksum: checksum(subagentThreadsSql) }
|
|
312
312
|
),
|
|
313
313
|
sqliteSql("079-subagent-questions", subagentQuestionsSql),
|
|
314
|
+
sqliteSql("080-agent-run-cancel-requests", agentRunCancelRequestsSql),
|
|
315
|
+
sqliteSql("081-agent-thread-runs-on", agentThreadRunsOnSql),
|
|
316
|
+
sqliteSql("082-agent-token-rates", agentTokenRatesSql),
|
|
317
|
+
sqliteSql("083-agent-computer-sessions", agentComputerSessionsSql),
|
|
314
318
|
],
|
|
315
319
|
})
|
|
316
320
|
|
|
@@ -709,3 +713,11 @@ import agentApprovalResumeSql from "./migrations/077-agent-approval-resume.sql"
|
|
|
709
713
|
}
|
|
710
714
|
import subagentThreadsSql from "./migrations/078-subagent-threads.sql" with { type: "text" }
|
|
711
715
|
import subagentQuestionsSql from "./migrations/079-subagent-questions.sql" with { type: "text" }
|
|
716
|
+
import agentRunCancelRequestsSql from "./migrations/080-agent-run-cancel-requests.sql" with {
|
|
717
|
+
type: "text",
|
|
718
|
+
}
|
|
719
|
+
import agentThreadRunsOnSql from "./migrations/081-agent-thread-runs-on.sql" with { type: "text" }
|
|
720
|
+
import agentTokenRatesSql from "./migrations/082-agent-token-rates.sql" with { type: "text" }
|
|
721
|
+
import agentComputerSessionsSql from "./migrations/083-agent-computer-sessions.sql" with {
|
|
722
|
+
type: "text",
|
|
723
|
+
}
|