@sixb/pg 0.1.21 → 0.1.22
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/README.md +1 -0
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +48 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +19 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +6 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.js +3720 -1594
- package/dist/index.js.map +37 -31
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +17 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +27 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +12 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +7 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +6 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +8 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-artifact-storage.d.ts +8 -1
- package/dist/pg-artifact-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +400 -0
- package/src/agents/checkpoints.ts +8 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +175 -0
- package/src/agents/messages.ts +14 -7
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +154 -22
- package/src/agents/settings.ts +105 -0
- package/src/agents/threads.ts +356 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/users.ts +105 -43
- package/src/migrations/069-artifact-kinds.sql +4 -0
- package/src/migrations/070-artifact-state.sql +16 -0
- package/src/migrations/071-agent-thread-participants.sql +16 -0
- package/src/migrations/072-forked-context-checkpoints.sql +23 -0
- package/src/migrations/073-unique-values.sql +25 -0
- package/src/migrations/074-generated-media-usage.sql +21 -0
- package/src/migrations/075-model-call-files.sql +11 -0
- package/src/migrations/076-agent-memory.sql +33 -0
- package/src/migrations/077-artifact-threads.sql +21 -0
- package/src/migrations/078-agent-approvals.sql +58 -0
- package/src/migrations/079-agent-approval-resume.sql +20 -0
- package/src/migrations/080-subagent-threads.sql +145 -0
- package/src/migrations/081-subagent-questions.sql +6 -0
- package/src/migrations.ts +34 -0
- package/src/objects/conditional-reader.ts +44 -0
- package/src/objects/prepare-queries.ts +1 -1
- package/src/objects/query-compiler.ts +96 -33
- package/src/objects/query-indexes.ts +29 -10
- package/src/objects/read-scope.ts +140 -25
- package/src/objects/reader.ts +50 -6
- package/src/objects/scoped-reader.ts +101 -0
- package/src/objects/selected-reader.ts +16 -76
- package/src/objects/storage.ts +24 -5
- package/src/objects/text-count-index.ts +1 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +8 -0
- package/src/ontology-storage/materialization-writer.ts +76 -1
- package/src/ontology-storage/materializations.ts +20 -1
- package/src/ontology-storage/unique-values.ts +274 -0
- package/src/pg-ai-cost-storage.ts +62 -12
- package/src/pg-ai-usage-storage.ts +90 -1
- package/src/pg-artifact-storage.ts +205 -9
- package/src/pg-execution-storage.ts +31 -0
- package/src/storage-errors.ts +7 -0
- package/src/transactions.ts +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uniquePropertyNotPrepared,
|
|
3
|
+
uniqueValueViolation,
|
|
4
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
5
|
+
import type {
|
|
6
|
+
ExactUniqueValueWrite,
|
|
7
|
+
OntologyUniqueValueStorage,
|
|
8
|
+
PreparedUniqueProperty,
|
|
9
|
+
UniqueValueEntry,
|
|
10
|
+
UniqueValueRef,
|
|
11
|
+
} from "@sixb/core/storage"
|
|
12
|
+
import type postgres from "postgres"
|
|
13
|
+
import type { SQLClient } from "../pg-client"
|
|
14
|
+
import { jsonParameter, type PgRootOperation } from "./shared"
|
|
15
|
+
|
|
16
|
+
type Query = postgres.PendingQuery<postgres.Row[]>
|
|
17
|
+
|
|
18
|
+
interface PgClaimCounts {
|
|
19
|
+
readonly staged: number | string
|
|
20
|
+
readonly written: number | string
|
|
21
|
+
readonly unprepared_type: string | null
|
|
22
|
+
readonly unprepared_property: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gives each candidate value to its object, set-based. `candidates` selects `object_type_id`,
|
|
27
|
+
* `property_id`, `value_key` and `primary_id`, and with `requirePrepared` also the claim's
|
|
28
|
+
* `key_kind` and `key_version`, which must match how the property was prepared.
|
|
29
|
+
*
|
|
30
|
+
* The insert goes first and conflicts do nothing, as the plan writer's statements do: only when
|
|
31
|
+
* it writes fewer rows than it was given does a second query look for the holder to name. A value
|
|
32
|
+
* another object holds, or that two candidates give to different objects, throws
|
|
33
|
+
* `UniqueValueViolationError`; a value already held by its own object is no conflict. A value a
|
|
34
|
+
* concurrent transaction claimed after this one's snapshot fails serialization instead, so the
|
|
35
|
+
* commit retries and then finds it held.
|
|
36
|
+
*/
|
|
37
|
+
export async function claimPgUniqueValues(
|
|
38
|
+
sql: SQLClient,
|
|
39
|
+
projectId: string,
|
|
40
|
+
candidates: Query,
|
|
41
|
+
options: { readonly requirePrepared: boolean }
|
|
42
|
+
): Promise<number> {
|
|
43
|
+
const unprepared = options.requirePrepared
|
|
44
|
+
? sql`
|
|
45
|
+
SELECT claimed.object_type_id, claimed.property_id
|
|
46
|
+
FROM (
|
|
47
|
+
SELECT DISTINCT object_type_id, property_id, key_kind, key_version FROM candidates
|
|
48
|
+
) AS claimed
|
|
49
|
+
WHERE NOT EXISTS (
|
|
50
|
+
SELECT 1 FROM object_unique_properties AS prepared
|
|
51
|
+
WHERE prepared.project_id = ${projectId}
|
|
52
|
+
AND prepared.object_type_id = claimed.object_type_id
|
|
53
|
+
AND prepared.property_id = claimed.property_id
|
|
54
|
+
AND prepared.key_kind = claimed.key_kind
|
|
55
|
+
AND prepared.key_version = claimed.key_version
|
|
56
|
+
)
|
|
57
|
+
LIMIT 1
|
|
58
|
+
`
|
|
59
|
+
: sql`SELECT NULL::text AS object_type_id, NULL::text AS property_id WHERE false`
|
|
60
|
+
|
|
61
|
+
const [counts] = await sql<PgClaimCounts[]>`
|
|
62
|
+
WITH candidates AS MATERIALIZED (${candidates}),
|
|
63
|
+
unprepared AS (${unprepared}),
|
|
64
|
+
written AS (
|
|
65
|
+
INSERT INTO object_unique_values (
|
|
66
|
+
project_id, object_type_id, property_id, value_hash, value_key, primary_id
|
|
67
|
+
)
|
|
68
|
+
SELECT ${projectId}, object_type_id, property_id,
|
|
69
|
+
sha256(convert_to(value_key, 'UTF8')), value_key, primary_id
|
|
70
|
+
FROM candidates
|
|
71
|
+
WHERE NOT EXISTS (SELECT 1 FROM unprepared)
|
|
72
|
+
ON CONFLICT DO NOTHING
|
|
73
|
+
RETURNING 1
|
|
74
|
+
)
|
|
75
|
+
SELECT (SELECT COUNT(*) FROM candidates) AS staged, (SELECT COUNT(*) FROM written) AS written,
|
|
76
|
+
(SELECT object_type_id FROM unprepared) AS unprepared_type,
|
|
77
|
+
(SELECT property_id FROM unprepared) AS unprepared_property
|
|
78
|
+
`
|
|
79
|
+
|
|
80
|
+
if (counts?.unprepared_type && counts.unprepared_property) {
|
|
81
|
+
throw uniquePropertyNotPrepared({
|
|
82
|
+
objectTypeId: counts.unprepared_type,
|
|
83
|
+
propertyId: counts.unprepared_property,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const written = Number(counts?.written ?? 0)
|
|
88
|
+
if (written !== Number(counts?.staged ?? 0)) await assertNoOtherHolder(sql, projectId, candidates)
|
|
89
|
+
return written
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Names a candidate whose value another object holds, now that every candidate was inserted. */
|
|
93
|
+
async function assertNoOtherHolder(
|
|
94
|
+
sql: SQLClient,
|
|
95
|
+
projectId: string,
|
|
96
|
+
candidates: Query
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const [conflict] = await sql<
|
|
99
|
+
{
|
|
100
|
+
readonly object_type_id: string
|
|
101
|
+
readonly property_id: string
|
|
102
|
+
readonly holder: string
|
|
103
|
+
readonly claimant: string
|
|
104
|
+
}[]
|
|
105
|
+
>`
|
|
106
|
+
WITH candidates AS MATERIALIZED (${candidates})
|
|
107
|
+
SELECT candidates.object_type_id, candidates.property_id,
|
|
108
|
+
held.primary_id AS holder, candidates.primary_id AS claimant
|
|
109
|
+
FROM candidates
|
|
110
|
+
CROSS JOIN LATERAL (${heldValue(sql, projectId)} LIMIT 1) AS held
|
|
111
|
+
WHERE held.primary_id <> candidates.primary_id
|
|
112
|
+
LIMIT 1
|
|
113
|
+
`
|
|
114
|
+
if (!conflict) return
|
|
115
|
+
|
|
116
|
+
throw uniqueValueViolation(
|
|
117
|
+
{ objectTypeId: conflict.object_type_id, propertyId: conflict.property_id },
|
|
118
|
+
conflict.holder,
|
|
119
|
+
conflict.claimant
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The stored row of `candidates`' value, by its whole primary key: a lateral lookup per candidate
|
|
125
|
+
* keeps the planner on the index whatever it guesses of the table.
|
|
126
|
+
*/
|
|
127
|
+
export function heldValue(sql: SQLClient, projectId: string) {
|
|
128
|
+
return sql`
|
|
129
|
+
SELECT stored.primary_id, stored.value_hash
|
|
130
|
+
FROM object_unique_values AS stored
|
|
131
|
+
WHERE stored.project_id = ${projectId}
|
|
132
|
+
AND stored.object_type_id = candidates.object_type_id
|
|
133
|
+
AND stored.property_id = candidates.property_id
|
|
134
|
+
AND stored.value_hash = sha256(convert_to(candidates.value_key, 'UTF8'))
|
|
135
|
+
AND stored.value_key = candidates.value_key
|
|
136
|
+
`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The objects holding these unique values; a value no object holds is left out. */
|
|
140
|
+
export async function pgUniqueHolders(
|
|
141
|
+
sql: SQLClient,
|
|
142
|
+
projectId: string,
|
|
143
|
+
values: readonly UniqueValueRef[]
|
|
144
|
+
): Promise<ExactUniqueValueWrite[]> {
|
|
145
|
+
if (values.length === 0) return []
|
|
146
|
+
const requested = values.map((value) => ({
|
|
147
|
+
object_type_id: value.objectTypeId,
|
|
148
|
+
property_id: value.propertyId,
|
|
149
|
+
value_key: value.valueKey,
|
|
150
|
+
}))
|
|
151
|
+
const rows = await sql<
|
|
152
|
+
{
|
|
153
|
+
readonly object_type_id: string
|
|
154
|
+
readonly property_id: string
|
|
155
|
+
readonly value_key: string
|
|
156
|
+
readonly primary_id: string
|
|
157
|
+
}[]
|
|
158
|
+
>`
|
|
159
|
+
WITH candidates AS MATERIALIZED (
|
|
160
|
+
SELECT * FROM jsonb_to_recordset(${jsonParameter(sql, requested)}::jsonb)
|
|
161
|
+
AS requested(object_type_id text, property_id text, value_key text)
|
|
162
|
+
)
|
|
163
|
+
SELECT candidates.object_type_id, candidates.property_id, candidates.value_key,
|
|
164
|
+
held.primary_id
|
|
165
|
+
FROM candidates
|
|
166
|
+
CROSS JOIN LATERAL (${heldValue(sql, projectId)} LIMIT 1) AS held
|
|
167
|
+
`
|
|
168
|
+
return rows.map((row) => ({
|
|
169
|
+
objectTypeId: row.object_type_id,
|
|
170
|
+
propertyId: row.property_id,
|
|
171
|
+
valueKey: row.value_key,
|
|
172
|
+
primaryId: row.primary_id,
|
|
173
|
+
}))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class PgOntologyUniqueValueStorage implements OntologyUniqueValueStorage {
|
|
177
|
+
constructor(private readonly runRootOperation: PgRootOperation) {}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* `LOCK TABLE` takes no snapshot, so as the transaction's first statement it waits before the
|
|
181
|
+
* serializable snapshot is taken, and a preparer that waited sees the values the previous one
|
|
182
|
+
* built instead of building them again. An advisory lock would be taken by a `SELECT`, after
|
|
183
|
+
* the snapshot. Only preparation writes this table, so claims, which read it, never wait.
|
|
184
|
+
*/
|
|
185
|
+
async lockPreparation(_input: { readonly projectId: string }): Promise<void> {
|
|
186
|
+
await this.runRootOperation(async (sql) => {
|
|
187
|
+
await sql`LOCK TABLE object_unique_properties IN SHARE ROW EXCLUSIVE MODE`
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async listPrepared(input: {
|
|
192
|
+
readonly projectId: string
|
|
193
|
+
}): Promise<readonly PreparedUniqueProperty[]> {
|
|
194
|
+
return this.runRootOperation(async (sql) => {
|
|
195
|
+
const rows = await sql<
|
|
196
|
+
{
|
|
197
|
+
readonly object_type_id: string
|
|
198
|
+
readonly property_id: string
|
|
199
|
+
readonly key_kind: PreparedUniqueProperty["keyKind"]
|
|
200
|
+
readonly key_version: number
|
|
201
|
+
}[]
|
|
202
|
+
>`
|
|
203
|
+
SELECT object_type_id, property_id, key_kind, key_version FROM object_unique_properties
|
|
204
|
+
WHERE project_id = ${input.projectId}
|
|
205
|
+
ORDER BY object_type_id, property_id
|
|
206
|
+
`
|
|
207
|
+
return rows.map((row) => ({
|
|
208
|
+
objectTypeId: row.object_type_id,
|
|
209
|
+
propertyId: row.property_id,
|
|
210
|
+
keyKind: row.key_kind,
|
|
211
|
+
keyVersion: row.key_version,
|
|
212
|
+
}))
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async clear(input: {
|
|
217
|
+
readonly projectId: string
|
|
218
|
+
readonly objectTypeId: string
|
|
219
|
+
readonly propertyId: string
|
|
220
|
+
}): Promise<void> {
|
|
221
|
+
await this.runRootOperation(async (sql) => {
|
|
222
|
+
await sql`
|
|
223
|
+
DELETE FROM object_unique_values
|
|
224
|
+
WHERE project_id = ${input.projectId} AND object_type_id = ${input.objectTypeId}
|
|
225
|
+
AND property_id = ${input.propertyId}
|
|
226
|
+
`
|
|
227
|
+
await sql`
|
|
228
|
+
DELETE FROM object_unique_properties
|
|
229
|
+
WHERE project_id = ${input.projectId} AND object_type_id = ${input.objectTypeId}
|
|
230
|
+
AND property_id = ${input.propertyId}
|
|
231
|
+
`
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async insert(input: {
|
|
236
|
+
readonly projectId: string
|
|
237
|
+
readonly objectTypeId: string
|
|
238
|
+
readonly propertyId: string
|
|
239
|
+
readonly entries: readonly UniqueValueEntry[]
|
|
240
|
+
}): Promise<void> {
|
|
241
|
+
await this.runRootOperation((sql) =>
|
|
242
|
+
claimPgUniqueValues(
|
|
243
|
+
sql,
|
|
244
|
+
input.projectId,
|
|
245
|
+
sql`
|
|
246
|
+
SELECT ${input.objectTypeId}::text AS object_type_id,
|
|
247
|
+
${input.propertyId}::text AS property_id,
|
|
248
|
+
entry->>'valueKey' AS value_key, entry->>'primaryId' AS primary_id
|
|
249
|
+
FROM jsonb_array_elements(${jsonParameter(sql, input.entries)}::jsonb) AS entry
|
|
250
|
+
`,
|
|
251
|
+
// The values are being prepared: nothing to require yet.
|
|
252
|
+
{ requirePrepared: false }
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async markPrepared(
|
|
258
|
+
input: PreparedUniqueProperty & { readonly projectId: string; readonly preparedAt: string }
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
await this.runRootOperation(async (sql) => {
|
|
261
|
+
await sql`
|
|
262
|
+
INSERT INTO object_unique_properties (
|
|
263
|
+
project_id, object_type_id, property_id, key_kind, key_version, prepared_at
|
|
264
|
+
) VALUES (
|
|
265
|
+
${input.projectId}, ${input.objectTypeId}, ${input.propertyId}, ${input.keyKind},
|
|
266
|
+
${input.keyVersion}, ${input.preparedAt}::timestamptz
|
|
267
|
+
)
|
|
268
|
+
ON CONFLICT (project_id, object_type_id, property_id)
|
|
269
|
+
DO UPDATE SET key_kind = EXCLUDED.key_kind, key_version = EXCLUDED.key_version,
|
|
270
|
+
prepared_at = EXCLUDED.prepared_at
|
|
271
|
+
`
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AiAccountingAggregateFragment,
|
|
3
3
|
type AiAccountingRecordSetItem,
|
|
4
|
+
aiAccountingModelKind,
|
|
4
5
|
aiModelCallCostDetails,
|
|
5
6
|
aiModelCallCostMatchesUsage,
|
|
6
7
|
buildAiAccountingOverviewFromFragments,
|
|
@@ -103,6 +104,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
103
104
|
WITH filtered AS (
|
|
104
105
|
SELECT usage.provider_id,
|
|
105
106
|
usage.requested_model_id,
|
|
107
|
+
usage.model_kind,
|
|
106
108
|
usage.occurred_at,
|
|
107
109
|
usage.input_tokens,
|
|
108
110
|
usage.output_tokens,
|
|
@@ -112,6 +114,10 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
112
114
|
usage.text_output_tokens,
|
|
113
115
|
usage.reasoning_output_tokens,
|
|
114
116
|
usage.audio_duration_ms,
|
|
117
|
+
usage.image_output_tokens,
|
|
118
|
+
usage.audio_output_tokens,
|
|
119
|
+
usage.output_images,
|
|
120
|
+
usage.input_characters,
|
|
115
121
|
usage.reporting_status,
|
|
116
122
|
CASE
|
|
117
123
|
WHEN direct_agent.kind = 'conversation' THEN 'agent'
|
|
@@ -174,6 +180,8 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
174
180
|
UNION ALL
|
|
175
181
|
SELECT 'model', provider_id, requested_model_id, NULL, NULL, classified.* FROM classified
|
|
176
182
|
UNION ALL
|
|
183
|
+
SELECT 'kind', model_kind, NULL, NULL, NULL, classified.* FROM classified
|
|
184
|
+
UNION ALL
|
|
177
185
|
SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, classified.*
|
|
178
186
|
FROM classified WHERE attribution_agent_kind IS NOT NULL
|
|
179
187
|
UNION ALL
|
|
@@ -206,7 +214,15 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
206
214
|
COUNT(reasoning_output_tokens)::text AS reasoning_output_tokens_count,
|
|
207
215
|
SUM(reasoning_output_tokens)::text AS reasoning_output_tokens_sum,
|
|
208
216
|
COUNT(audio_duration_ms)::text AS audio_duration_ms_count,
|
|
217
|
+
COUNT(image_output_tokens)::text AS image_output_tokens_count,
|
|
218
|
+
COUNT(audio_output_tokens)::text AS audio_output_tokens_count,
|
|
219
|
+
COUNT(output_images)::text AS output_images_count,
|
|
220
|
+
COUNT(input_characters)::text AS input_characters_count,
|
|
209
221
|
SUM(audio_duration_ms)::text AS audio_duration_ms_sum,
|
|
222
|
+
SUM(image_output_tokens)::text AS image_output_tokens_sum,
|
|
223
|
+
SUM(audio_output_tokens)::text AS audio_output_tokens_sum,
|
|
224
|
+
SUM(output_images)::text AS output_images_sum,
|
|
225
|
+
SUM(input_characters)::text AS input_characters_sum,
|
|
210
226
|
COUNT(*) FILTER (WHERE valuation_status = 'rated')::text AS rated_call_count,
|
|
211
227
|
COUNT(*) FILTER (WHERE valuation_status = 'unpriceable')::text
|
|
212
228
|
AS unpriceable_call_count,
|
|
@@ -347,6 +363,10 @@ interface UsageRow {
|
|
|
347
363
|
readonly text_output_tokens: number | string | null
|
|
348
364
|
readonly reasoning_output_tokens: number | string | null
|
|
349
365
|
readonly audio_duration_ms: number | string | null
|
|
366
|
+
readonly image_output_tokens: number | string | null
|
|
367
|
+
readonly audio_output_tokens: number | string | null
|
|
368
|
+
readonly output_images: number | string | null
|
|
369
|
+
readonly input_characters: number | string | null
|
|
350
370
|
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
351
371
|
readonly raw_usage: ReadonlyJsonObject | string | null
|
|
352
372
|
readonly provider_ids: Record<string, unknown> | string | null
|
|
@@ -402,7 +422,15 @@ interface AggregateRow {
|
|
|
402
422
|
readonly reasoning_output_tokens_count: string
|
|
403
423
|
readonly reasoning_output_tokens_sum: string | null
|
|
404
424
|
readonly audio_duration_ms_count: string
|
|
425
|
+
readonly image_output_tokens_count: string
|
|
426
|
+
readonly audio_output_tokens_count: string
|
|
427
|
+
readonly output_images_count: string
|
|
428
|
+
readonly input_characters_count: string
|
|
405
429
|
readonly audio_duration_ms_sum: string | null
|
|
430
|
+
readonly image_output_tokens_sum: string | null
|
|
431
|
+
readonly audio_output_tokens_sum: string | null
|
|
432
|
+
readonly output_images_sum: string | null
|
|
433
|
+
readonly input_characters_sum: string | null
|
|
406
434
|
readonly rated_call_count: string
|
|
407
435
|
readonly unpriceable_call_count: string
|
|
408
436
|
readonly unvalued_call_count: string
|
|
@@ -447,18 +475,20 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
447
475
|
const dimensionKeys =
|
|
448
476
|
row.dimension === "model"
|
|
449
477
|
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
450
|
-
: row.dimension === "
|
|
451
|
-
?
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
: row.dimension === "
|
|
460
|
-
? {
|
|
461
|
-
:
|
|
478
|
+
: row.dimension === "kind"
|
|
479
|
+
? withModelKind(aiAccountingModelKind(row.key_1))
|
|
480
|
+
: row.dimension === "agent"
|
|
481
|
+
? {
|
|
482
|
+
agentKind: required(row.key_1) as "agent" | "workflowAgent",
|
|
483
|
+
...(row.key_1 === "workflowAgent"
|
|
484
|
+
? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
|
|
485
|
+
: {}),
|
|
486
|
+
}
|
|
487
|
+
: row.dimension === "workflow"
|
|
488
|
+
? { workflowId: required(row.key_1) }
|
|
489
|
+
: row.dimension === "series"
|
|
490
|
+
? { start: new Date(required(row.bucket_start)) }
|
|
491
|
+
: {}
|
|
462
492
|
return {
|
|
463
493
|
dimension: row.dimension,
|
|
464
494
|
...dimensionKeys,
|
|
@@ -486,6 +516,10 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
486
516
|
row.reasoning_output_tokens_sum
|
|
487
517
|
),
|
|
488
518
|
audioDurationMs: meterFragment(row.audio_duration_ms_count, row.audio_duration_ms_sum),
|
|
519
|
+
imageOutputTokens: meterFragment(row.image_output_tokens_count, row.image_output_tokens_sum),
|
|
520
|
+
audioOutputTokens: meterFragment(row.audio_output_tokens_count, row.audio_output_tokens_sum),
|
|
521
|
+
outputImages: meterFragment(row.output_images_count, row.output_images_sum),
|
|
522
|
+
inputCharacters: meterFragment(row.input_characters_count, row.input_characters_sum),
|
|
489
523
|
},
|
|
490
524
|
costs: {
|
|
491
525
|
...(row.amount_currency === null || row.amount_nanos === null
|
|
@@ -587,6 +621,18 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
|
|
|
587
621
|
...(number(row.audio_duration_ms) === undefined
|
|
588
622
|
? {}
|
|
589
623
|
: { audioDurationMs: number(row.audio_duration_ms) }),
|
|
624
|
+
...(number(row.image_output_tokens) === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: { imageOutputTokens: number(row.image_output_tokens) }),
|
|
627
|
+
...(number(row.audio_output_tokens) === undefined
|
|
628
|
+
? {}
|
|
629
|
+
: { audioOutputTokens: number(row.audio_output_tokens) }),
|
|
630
|
+
...(number(row.output_images) === undefined
|
|
631
|
+
? {}
|
|
632
|
+
: { outputImages: number(row.output_images) }),
|
|
633
|
+
...(number(row.input_characters) === undefined
|
|
634
|
+
? {}
|
|
635
|
+
: { inputCharacters: number(row.input_characters) }),
|
|
590
636
|
reportingStatus: row.reporting_status,
|
|
591
637
|
},
|
|
592
638
|
...(row.raw_usage === null ? {} : { rawUsage: parseJsonObject(row.raw_usage) }),
|
|
@@ -777,3 +823,7 @@ function required<T>(value: T | null | undefined): T {
|
|
|
777
823
|
}
|
|
778
824
|
return value
|
|
779
825
|
}
|
|
826
|
+
|
|
827
|
+
function withModelKind(modelKind: AiAccountingAggregateFragment["modelKind"]) {
|
|
828
|
+
return modelKind === undefined ? {} : { modelKind }
|
|
829
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FileRef } from "@sixb/core/blob-storage/server"
|
|
2
|
+
import {
|
|
3
|
+
assertAiUsageExecutionId,
|
|
4
|
+
assertSameCallExecution,
|
|
5
|
+
normalizeAiModelCallFilesRecord,
|
|
6
|
+
normalizeAiModelCallRecord,
|
|
7
|
+
} from "@sixb/core/internal/storage"
|
|
2
8
|
import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
|
|
3
9
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
4
10
|
import {
|
|
11
|
+
type AiModelCallFilesRecord,
|
|
5
12
|
type AiModelCallUsageInput,
|
|
6
13
|
type AiModelCallUsageRecord,
|
|
7
14
|
type AiUsageExecutionSummary,
|
|
8
15
|
type AiUsageStorage,
|
|
9
16
|
AiUsageStorageError,
|
|
10
17
|
aggregateAiModelCallUsage,
|
|
18
|
+
type GetAiModelCallFilesInput,
|
|
11
19
|
type GetLatestAiModelCallForExecutionInput,
|
|
20
|
+
type RecordAiModelCallFilesInput,
|
|
12
21
|
type RecordAiModelCallInput,
|
|
13
22
|
type RecordAiModelCallResult,
|
|
14
23
|
type SummarizeAiUsageExecutionInput,
|
|
@@ -51,6 +60,10 @@ export class PgAiUsageStorage implements AiUsageStorage {
|
|
|
51
60
|
text_output_tokens,
|
|
52
61
|
reasoning_output_tokens,
|
|
53
62
|
audio_duration_ms,
|
|
63
|
+
image_output_tokens,
|
|
64
|
+
audio_output_tokens,
|
|
65
|
+
output_images,
|
|
66
|
+
input_characters,
|
|
54
67
|
reporting_status,
|
|
55
68
|
raw_usage,
|
|
56
69
|
occurred_at,
|
|
@@ -81,6 +94,10 @@ export class PgAiUsageStorage implements AiUsageStorage {
|
|
|
81
94
|
${record.usage.textOutputTokens ?? null},
|
|
82
95
|
${record.usage.reasoningOutputTokens ?? null},
|
|
83
96
|
${record.usage.audioDurationMs ?? null},
|
|
97
|
+
${record.usage.imageOutputTokens ?? null},
|
|
98
|
+
${record.usage.audioOutputTokens ?? null},
|
|
99
|
+
${record.usage.outputImages ?? null},
|
|
100
|
+
${record.usage.inputCharacters ?? null},
|
|
84
101
|
${record.usage.reportingStatus},
|
|
85
102
|
${record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage)}::text::jsonb,
|
|
86
103
|
${record.occurredAt},
|
|
@@ -235,6 +252,48 @@ export class PgAiUsageStorage implements AiUsageStorage {
|
|
|
235
252
|
`
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
async recordModelCallFiles(input: RecordAiModelCallFilesInput): Promise<AiModelCallFilesRecord> {
|
|
256
|
+
const record = normalizeAiModelCallFilesRecord(input)
|
|
257
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
258
|
+
const existing = await this.findCallFiles(tx, record.projectId, record.callId)
|
|
259
|
+
if (existing) return assertSameCallExecution(existing, record)
|
|
260
|
+
await this.assertExecutionExists(tx, record.projectId, record.executionId)
|
|
261
|
+
const inserted = await tx<CallFilesRow[]>`
|
|
262
|
+
INSERT INTO ai_model_call_files (project_id, call_id, execution_id, files, recorded_at)
|
|
263
|
+
VALUES (
|
|
264
|
+
${record.projectId},
|
|
265
|
+
${record.callId},
|
|
266
|
+
${record.executionId},
|
|
267
|
+
${JSON.stringify(record.files)}::text::jsonb,
|
|
268
|
+
${record.recordedAt}
|
|
269
|
+
)
|
|
270
|
+
ON CONFLICT (project_id, call_id) DO NOTHING
|
|
271
|
+
RETURNING *
|
|
272
|
+
`
|
|
273
|
+
if (inserted[0]) return callFilesFromRow(inserted[0])
|
|
274
|
+
// Another writer recorded this call between the read and the insert.
|
|
275
|
+
const concurrent = await this.findCallFiles(tx, record.projectId, record.callId)
|
|
276
|
+
if (!concurrent) throw new Error("[SixbPg] AI model-call files disappeared after a conflict.")
|
|
277
|
+
return assertSameCallExecution(concurrent, record)
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getModelCallFiles(input: GetAiModelCallFilesInput): Promise<AiModelCallFilesRecord | null> {
|
|
282
|
+
assertNonBlankProjectId(input.projectId)
|
|
283
|
+
return this.findCallFiles(this.sql, input.projectId, input.callId)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async findCallFiles(
|
|
287
|
+
sql: PgStoreClient,
|
|
288
|
+
projectId: string,
|
|
289
|
+
callId: string
|
|
290
|
+
): Promise<AiModelCallFilesRecord | null> {
|
|
291
|
+
const [row] = await sql<CallFilesRow[]>`
|
|
292
|
+
SELECT * FROM ai_model_call_files WHERE project_id = ${projectId} AND call_id = ${callId}
|
|
293
|
+
`
|
|
294
|
+
return row ? callFilesFromRow(row) : null
|
|
295
|
+
}
|
|
296
|
+
|
|
238
297
|
private async assertExecutionExists(
|
|
239
298
|
sql: PgStoreClient,
|
|
240
299
|
projectId: string,
|
|
@@ -253,6 +312,24 @@ export class PgAiUsageStorage implements AiUsageStorage {
|
|
|
253
312
|
}
|
|
254
313
|
}
|
|
255
314
|
|
|
315
|
+
interface CallFilesRow {
|
|
316
|
+
readonly project_id: string
|
|
317
|
+
readonly call_id: string
|
|
318
|
+
readonly execution_id: string
|
|
319
|
+
readonly files: FileRef[] | string
|
|
320
|
+
readonly recorded_at: Date | string
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function callFilesFromRow(row: CallFilesRow): AiModelCallFilesRecord {
|
|
324
|
+
return {
|
|
325
|
+
projectId: row.project_id,
|
|
326
|
+
executionId: row.execution_id,
|
|
327
|
+
callId: row.call_id,
|
|
328
|
+
files: typeof row.files === "string" ? (JSON.parse(row.files) as FileRef[]) : row.files,
|
|
329
|
+
recordedAt: new Date(row.recorded_at),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
256
333
|
interface AiUsageRow {
|
|
257
334
|
readonly project_id: string
|
|
258
335
|
readonly id: string
|
|
@@ -275,6 +352,10 @@ interface AiUsageRow {
|
|
|
275
352
|
readonly text_output_tokens: number | string | null
|
|
276
353
|
readonly reasoning_output_tokens: number | string | null
|
|
277
354
|
readonly audio_duration_ms: number | string | null
|
|
355
|
+
readonly image_output_tokens: number | string | null
|
|
356
|
+
readonly audio_output_tokens: number | string | null
|
|
357
|
+
readonly output_images: number | string | null
|
|
358
|
+
readonly input_characters: number | string | null
|
|
278
359
|
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
279
360
|
readonly raw_usage: ReadonlyJsonObject | string | null
|
|
280
361
|
readonly occurred_at: Date | string
|
|
@@ -319,6 +400,14 @@ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
|
|
|
319
400
|
? {}
|
|
320
401
|
: { reasoningOutputTokens: Number(row.reasoning_output_tokens) }),
|
|
321
402
|
...(row.audio_duration_ms === null ? {} : { audioDurationMs: Number(row.audio_duration_ms) }),
|
|
403
|
+
...(row.image_output_tokens === null
|
|
404
|
+
? {}
|
|
405
|
+
: { imageOutputTokens: Number(row.image_output_tokens) }),
|
|
406
|
+
...(row.audio_output_tokens === null
|
|
407
|
+
? {}
|
|
408
|
+
: { audioOutputTokens: Number(row.audio_output_tokens) }),
|
|
409
|
+
...(row.output_images === null ? {} : { outputImages: Number(row.output_images) }),
|
|
410
|
+
...(row.input_characters === null ? {} : { inputCharacters: Number(row.input_characters) }),
|
|
322
411
|
}
|
|
323
412
|
}
|
|
324
413
|
|