@sixb/pg 0.1.5 → 0.1.7
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/rows.d.ts +8 -5
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts +5 -0
- package/dist/ai-cost-groups.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +4 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts +29 -0
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +19 -1
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1804 -464
- package/dist/index.js.map +19 -16
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +7 -3
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts +2 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-limit-storage.d.ts +34 -0
- package/dist/pg-ai-limit-storage.d.ts.map +1 -0
- package/dist/pg-ai-usage-storage.d.ts +28 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agents/rows.ts +38 -9
- package/src/agents/runs.ts +148 -11
- package/src/agents/threads.ts +1 -17
- package/src/ai-cost-groups.ts +102 -0
- package/src/auth-storage/auth-storage.ts +84 -0
- package/src/auth-storage/device-authorizations.ts +131 -0
- package/src/auth-storage/rows.ts +41 -0
- package/src/auth-storage/shared.ts +1 -0
- package/src/index.ts +8 -1
- package/src/migrations/029-model-accounting.sql +24 -0
- package/src/migrations/030-ai-usage-limits.sql +94 -0
- package/src/migrations/031-subagent-runs.sql +97 -0
- package/src/migrations/032-conversation-run-spec.sql +23 -0
- package/src/migrations/033-retire-agent-definitions.sql +28 -0
- package/src/migrations/034-device-authorizations.sql +24 -0
- package/src/migrations.ts +18 -0
- package/src/ontology-storage/materialization-state.ts +45 -29
- package/src/ontology-storage/materializations.ts +3 -3
- package/src/pg-ai-cost-storage.ts +71 -24
- package/src/pg-ai-limit-storage.ts +901 -0
- package/src/pg-ai-usage-storage.ts +34 -0
- package/src/pg-workflow-run-storage.ts +8 -8
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- Agent executions may either use a managed service account or inherit their parent's exact
|
|
2
|
+
-- credentialed-user / auth-disabled authority.
|
|
3
|
+
ALTER TABLE executions
|
|
4
|
+
DROP CONSTRAINT executions_check3,
|
|
5
|
+
ADD CONSTRAINT executions_agent_authority_check CHECK (
|
|
6
|
+
(
|
|
7
|
+
executor_kind = 'request'
|
|
8
|
+
AND source_kind = 'http'
|
|
9
|
+
AND executor_id = source_id
|
|
10
|
+
AND (
|
|
11
|
+
(
|
|
12
|
+
authority_kind = 'principal'
|
|
13
|
+
AND requested_by_user_id IS NOT DISTINCT FROM authority_user_id
|
|
14
|
+
AND requested_by_service_account_id IS NOT DISTINCT FROM authority_service_account_id
|
|
15
|
+
)
|
|
16
|
+
OR (
|
|
17
|
+
authority_kind = 'disabled'
|
|
18
|
+
AND requested_by_user_id IS NULL
|
|
19
|
+
AND requested_by_service_account_id IS NULL
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
OR (
|
|
24
|
+
executor_kind IN ('action', 'pipeline', 'projection', 'rule', 'sync', 'webhook', 'workflow')
|
|
25
|
+
AND authority_kind = 'trustedPrimitive'
|
|
26
|
+
AND executor_kind = authority_primitive_kind
|
|
27
|
+
)
|
|
28
|
+
OR (
|
|
29
|
+
executor_kind = 'agent'
|
|
30
|
+
AND source_kind = 'execution'
|
|
31
|
+
AND (
|
|
32
|
+
(
|
|
33
|
+
authority_kind = 'principal'
|
|
34
|
+
AND authority_service_account_id IS NOT NULL
|
|
35
|
+
AND authority_session_id IS NULL
|
|
36
|
+
AND authority_access_token_id IS NULL
|
|
37
|
+
)
|
|
38
|
+
OR (
|
|
39
|
+
authority_kind = 'principal'
|
|
40
|
+
AND authority_user_id IS NOT NULL
|
|
41
|
+
AND num_nonnulls(authority_session_id, authority_access_token_id) = 1
|
|
42
|
+
)
|
|
43
|
+
OR authority_kind = 'disabled'
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
OR (executor_kind = 'kernel' AND authority_kind = 'kernel')
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
ALTER TABLE agent_runs
|
|
50
|
+
ADD COLUMN kind TEXT NOT NULL DEFAULT 'conversation'
|
|
51
|
+
CHECK (kind IN ('conversation', 'subagent')),
|
|
52
|
+
ADD COLUMN parent_run_id TEXT,
|
|
53
|
+
ADD COLUMN spawn_key TEXT,
|
|
54
|
+
ADD COLUMN spec JSONB,
|
|
55
|
+
ADD COLUMN result JSONB,
|
|
56
|
+
ALTER COLUMN thread_id DROP NOT NULL,
|
|
57
|
+
ALTER COLUMN agent_id DROP NOT NULL,
|
|
58
|
+
ALTER COLUMN trigger_message_id DROP NOT NULL,
|
|
59
|
+
ADD CONSTRAINT fk_agent_runs_parent
|
|
60
|
+
FOREIGN KEY (project_id, parent_run_id)
|
|
61
|
+
REFERENCES agent_runs (project_id, id)
|
|
62
|
+
ON DELETE RESTRICT,
|
|
63
|
+
ADD CONSTRAINT agent_runs_kind_fields CHECK (
|
|
64
|
+
(
|
|
65
|
+
kind = 'conversation'
|
|
66
|
+
AND thread_id IS NOT NULL
|
|
67
|
+
AND agent_id IS NOT NULL
|
|
68
|
+
AND trigger_message_id IS NOT NULL
|
|
69
|
+
AND parent_run_id IS NULL
|
|
70
|
+
AND spawn_key IS NULL
|
|
71
|
+
AND spec IS NULL
|
|
72
|
+
AND result IS NULL
|
|
73
|
+
)
|
|
74
|
+
OR
|
|
75
|
+
(
|
|
76
|
+
kind = 'subagent'
|
|
77
|
+
AND thread_id IS NULL
|
|
78
|
+
AND agent_id IS NULL
|
|
79
|
+
AND trigger_message_id IS NULL
|
|
80
|
+
AND parent_run_id IS NOT NULL
|
|
81
|
+
AND spawn_key IS NOT NULL
|
|
82
|
+
AND spec IS NOT NULL
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
ADD CONSTRAINT agent_runs_subagent_result CHECK (
|
|
86
|
+
kind <> 'subagent' OR status <> 'succeeded' OR result IS NOT NULL
|
|
87
|
+
),
|
|
88
|
+
ADD CONSTRAINT uq_agent_runs_parent_spawn_key UNIQUE (project_id, parent_run_id, spawn_key);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX idx_agent_runs_project_parent_started
|
|
91
|
+
ON agent_runs (
|
|
92
|
+
project_id,
|
|
93
|
+
parent_run_id,
|
|
94
|
+
status,
|
|
95
|
+
COALESCE(started_at, created_at) DESC,
|
|
96
|
+
id DESC
|
|
97
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
ALTER TABLE agent_runs
|
|
2
|
+
DROP CONSTRAINT agent_runs_kind_fields,
|
|
3
|
+
ADD CONSTRAINT agent_runs_kind_fields CHECK (
|
|
4
|
+
(
|
|
5
|
+
kind = 'conversation'
|
|
6
|
+
AND thread_id IS NOT NULL
|
|
7
|
+
AND agent_id IS NOT NULL
|
|
8
|
+
AND trigger_message_id IS NOT NULL
|
|
9
|
+
AND parent_run_id IS NULL
|
|
10
|
+
AND spawn_key IS NULL
|
|
11
|
+
AND result IS NULL
|
|
12
|
+
)
|
|
13
|
+
OR
|
|
14
|
+
(
|
|
15
|
+
kind = 'subagent'
|
|
16
|
+
AND thread_id IS NULL
|
|
17
|
+
AND agent_id IS NULL
|
|
18
|
+
AND trigger_message_id IS NULL
|
|
19
|
+
AND parent_run_id IS NOT NULL
|
|
20
|
+
AND spawn_key IS NOT NULL
|
|
21
|
+
AND spec IS NOT NULL
|
|
22
|
+
)
|
|
23
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
DROP INDEX IF EXISTS idx_agent_threads_project_agent_activity;
|
|
2
|
+
ALTER TABLE workflow_agent_node_runs RENAME COLUMN agent_id TO actor_id;
|
|
3
|
+
DROP INDEX IF EXISTS idx_agent_runs_project_agent_started;
|
|
4
|
+
|
|
5
|
+
ALTER TABLE agent_threads DROP COLUMN agent_id;
|
|
6
|
+
|
|
7
|
+
ALTER TABLE agent_runs
|
|
8
|
+
DROP CONSTRAINT agent_runs_kind_fields,
|
|
9
|
+
DROP COLUMN agent_id,
|
|
10
|
+
ADD CONSTRAINT agent_runs_kind_fields CHECK (
|
|
11
|
+
(
|
|
12
|
+
kind = 'conversation'
|
|
13
|
+
AND thread_id IS NOT NULL
|
|
14
|
+
AND trigger_message_id IS NOT NULL
|
|
15
|
+
AND parent_run_id IS NULL
|
|
16
|
+
AND spawn_key IS NULL
|
|
17
|
+
AND result IS NULL
|
|
18
|
+
)
|
|
19
|
+
OR
|
|
20
|
+
(
|
|
21
|
+
kind = 'subagent'
|
|
22
|
+
AND thread_id IS NULL
|
|
23
|
+
AND trigger_message_id IS NULL
|
|
24
|
+
AND parent_run_id IS NOT NULL
|
|
25
|
+
AND spawn_key IS NOT NULL
|
|
26
|
+
AND spec IS NOT NULL
|
|
27
|
+
)
|
|
28
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
CREATE TABLE auth_device_authorizations (
|
|
2
|
+
project_id TEXT NOT NULL,
|
|
3
|
+
id TEXT NOT NULL,
|
|
4
|
+
device_code_hash TEXT NOT NULL,
|
|
5
|
+
user_code TEXT NOT NULL,
|
|
6
|
+
client_name TEXT NOT NULL,
|
|
7
|
+
token_name TEXT NOT NULL,
|
|
8
|
+
token_expires_at TIMESTAMPTZ NOT NULL,
|
|
9
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'denied', 'consumed')),
|
|
10
|
+
approved_user_id TEXT,
|
|
11
|
+
approved_session_id TEXT,
|
|
12
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
13
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
14
|
+
approved_at TIMESTAMPTZ,
|
|
15
|
+
denied_at TIMESTAMPTZ,
|
|
16
|
+
consumed_at TIMESTAMPTZ,
|
|
17
|
+
PRIMARY KEY (project_id, id),
|
|
18
|
+
UNIQUE (project_id, user_code),
|
|
19
|
+
FOREIGN KEY (project_id, approved_user_id) REFERENCES auth_users (project_id, id),
|
|
20
|
+
FOREIGN KEY (project_id, approved_session_id) REFERENCES auth_sessions (project_id, id)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX idx_auth_device_authorizations_pending
|
|
24
|
+
ON auth_device_authorizations (project_id, status, expires_at DESC);
|
package/src/migrations.ts
CHANGED
|
@@ -73,6 +73,18 @@ import agentContextCheckpointsSql from "./migrations/027-agent-context-checkpoin
|
|
|
73
73
|
import objectOverrideEditTimesSql from "./migrations/028-object-override-edit-times.sql" with {
|
|
74
74
|
type: "text",
|
|
75
75
|
}
|
|
76
|
+
import modelAccountingSql from "./migrations/029-model-accounting.sql" with { type: "text" }
|
|
77
|
+
import aiUsageLimitsSql from "./migrations/030-ai-usage-limits.sql" with { type: "text" }
|
|
78
|
+
import subagentRunsSql from "./migrations/031-subagent-runs.sql" with { type: "text" }
|
|
79
|
+
import conversationRunSpecSql from "./migrations/032-conversation-run-spec.sql" with {
|
|
80
|
+
type: "text",
|
|
81
|
+
}
|
|
82
|
+
import retireAgentDefinitionsSql from "./migrations/033-retire-agent-definitions.sql" with {
|
|
83
|
+
type: "text",
|
|
84
|
+
}
|
|
85
|
+
import deviceAuthorizationsSql from "./migrations/034-device-authorizations.sql" with {
|
|
86
|
+
type: "text",
|
|
87
|
+
}
|
|
76
88
|
import type { SQL, SQLClient } from "./pg-client"
|
|
77
89
|
|
|
78
90
|
export interface PostgresMigrationContext {
|
|
@@ -354,6 +366,12 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
|
|
|
354
366
|
pgSql("026-ai-cost-accounting", aiCostAccountingSql),
|
|
355
367
|
pgSql("027-agent-context-checkpoints", agentContextCheckpointsSql),
|
|
356
368
|
pgSql("028-object-override-edit-times", objectOverrideEditTimesSql),
|
|
369
|
+
pgSql("029-model-accounting", modelAccountingSql),
|
|
370
|
+
pgSql("030-ai-usage-limits", aiUsageLimitsSql),
|
|
371
|
+
pgSql("031-subagent-runs", subagentRunsSql),
|
|
372
|
+
pgSql("032-conversation-run-spec", conversationRunSpecSql),
|
|
373
|
+
pgSql("033-retire-agent-definitions", retireAgentDefinitionsSql),
|
|
374
|
+
pgSql("034-device-authorizations", deviceAuthorizationsSql),
|
|
357
375
|
],
|
|
358
376
|
})
|
|
359
377
|
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
startScopeAccumulator,
|
|
23
23
|
} from "@sixb/core/internal/ontology-storage-provider"
|
|
24
24
|
import type {
|
|
25
|
+
MaterializationLinkScopeRevision,
|
|
25
26
|
MaterializationLinkScopeState,
|
|
26
27
|
MaterializationLinkState,
|
|
27
28
|
MaterializationObjectState,
|
|
@@ -407,7 +408,14 @@ export class PgMaterializationStateReader {
|
|
|
407
408
|
})
|
|
408
409
|
}
|
|
409
410
|
|
|
410
|
-
async
|
|
411
|
+
async linkScopeRevisions(
|
|
412
|
+
scopes: readonly { readonly source: OntologyObjectRef; readonly linkId: string }[]
|
|
413
|
+
): Promise<readonly MaterializationLinkScopeRevision[]> {
|
|
414
|
+
if (scopes.length === 0) return []
|
|
415
|
+
return (await this.readEffectiveLinkScopes(scopes)).revisions
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async linkSlotStates(
|
|
411
419
|
scopes: readonly { readonly source: OntologyObjectRef; readonly linkId: string }[]
|
|
412
420
|
): Promise<readonly MaterializationLinkScopeState[]> {
|
|
413
421
|
if (scopes.length === 0) return []
|
|
@@ -417,12 +425,6 @@ export class PgMaterializationStateReader {
|
|
|
417
425
|
source_id: source.primaryId,
|
|
418
426
|
link_id: linkId,
|
|
419
427
|
}))
|
|
420
|
-
const accumulators = new Map(
|
|
421
|
-
scopes.map(({ source, linkId }) => {
|
|
422
|
-
const key = linkScopeSortKey(source, linkId)
|
|
423
|
-
return [key, startScopeAccumulator(source, linkId)] as const
|
|
424
|
-
})
|
|
425
|
-
)
|
|
426
428
|
const requestedParameter = jsonParameter(this.sql, requested)
|
|
427
429
|
const [sourceRows, slotOverrideRows] = await Promise.all([
|
|
428
430
|
this.sql<PgOntologySourceAssertionRow[]>`
|
|
@@ -470,6 +472,37 @@ export class PgMaterializationStateReader {
|
|
|
470
472
|
return [linkScopeSortKey(stored.ref.source, stored.ref.linkId), stored] as const
|
|
471
473
|
})
|
|
472
474
|
)
|
|
475
|
+
const { revisions, effective } = await this.readEffectiveLinkScopes(scopes)
|
|
476
|
+
return scopes.map(({ source, linkId }, index) => {
|
|
477
|
+
const key = linkScopeSortKey(source, linkId)
|
|
478
|
+
return {
|
|
479
|
+
...revisions[index]!,
|
|
480
|
+
sourceAssertion: sources.get(key) ?? null,
|
|
481
|
+
override: overrides.get(key) ?? null,
|
|
482
|
+
effective: effective.get(key) ?? null,
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private async readEffectiveLinkScopes(
|
|
488
|
+
scopes: readonly { readonly source: OntologyObjectRef; readonly linkId: string }[]
|
|
489
|
+
): Promise<{
|
|
490
|
+
readonly revisions: readonly MaterializationLinkScopeRevision[]
|
|
491
|
+
readonly effective: ReadonlyMap<string, EffectiveLinkSnapshot | null>
|
|
492
|
+
}> {
|
|
493
|
+
const requested = scopes.map(({ source, linkId }) => ({
|
|
494
|
+
scope_sort_key: linkScopeSortKey(source, linkId),
|
|
495
|
+
source_type_id: source.objectTypeId,
|
|
496
|
+
source_id: source.primaryId,
|
|
497
|
+
link_id: linkId,
|
|
498
|
+
}))
|
|
499
|
+
const requestedParameter = jsonParameter(this.sql, requested)
|
|
500
|
+
const accumulators = new Map(
|
|
501
|
+
scopes.map(({ source, linkId }) => {
|
|
502
|
+
const key = linkScopeSortKey(source, linkId)
|
|
503
|
+
return [key, startScopeAccumulator(source, linkId)] as const
|
|
504
|
+
})
|
|
505
|
+
)
|
|
473
506
|
const effective = new Map<string, EffectiveLinkSnapshot | null>()
|
|
474
507
|
let scopeCursor: string | null = null
|
|
475
508
|
let linkCursor: string | null = null
|
|
@@ -510,29 +543,12 @@ export class PgMaterializationStateReader {
|
|
|
510
543
|
scopeCursor = last.scope_sort_key
|
|
511
544
|
linkCursor = last.link_sort_key
|
|
512
545
|
}
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
override: overrides.get(key) ?? null,
|
|
519
|
-
effective: effective.get(key) ?? null,
|
|
520
|
-
}
|
|
521
|
-
})
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async linkScope(
|
|
525
|
-
source: OntologyObjectRef,
|
|
526
|
-
linkId: string
|
|
527
|
-
): Promise<MaterializationLinkScopeState> {
|
|
528
|
-
const [scope] = await this.linkScopes([{ source, linkId }])
|
|
529
|
-
if (!scope) {
|
|
530
|
-
throw new MaterializationConflictError(
|
|
531
|
-
"effective-state",
|
|
532
|
-
"Link scope lookup returned no row."
|
|
533
|
-
)
|
|
546
|
+
return {
|
|
547
|
+
revisions: scopes.map(({ source, linkId }) =>
|
|
548
|
+
finishScopeAccumulator(accumulators.get(linkScopeSortKey(source, linkId))!)
|
|
549
|
+
),
|
|
550
|
+
effective,
|
|
534
551
|
}
|
|
535
|
-
return scope
|
|
536
552
|
}
|
|
537
553
|
|
|
538
554
|
async *incidentLinks(
|
|
@@ -121,9 +121,9 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
121
121
|
for (const expected of input.expected.links) {
|
|
122
122
|
this.assertLink(linkRevisions.get(linkRefKey(expected.ref)), expected)
|
|
123
123
|
}
|
|
124
|
-
const
|
|
124
|
+
const linkScopeRevisions = await reader.linkScopeRevisions(input.expected.linkScopes)
|
|
125
125
|
for (const [index, expected] of input.expected.linkScopes.entries()) {
|
|
126
|
-
if (
|
|
126
|
+
if (linkScopeRevisions[index]?.fingerprint !== expected.fingerprint) {
|
|
127
127
|
throw new MaterializationConflictError(
|
|
128
128
|
"effective-state",
|
|
129
129
|
`Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`
|
|
@@ -191,7 +191,7 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
191
191
|
yield {
|
|
192
192
|
objects: [],
|
|
193
193
|
links: [],
|
|
194
|
-
linkScopes: await reader.
|
|
194
|
+
linkScopes: await reader.linkSlotStates(scopes.slice(offset, offset + input.pageRows)),
|
|
195
195
|
points: [],
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseAiModelCallCostDetails,
|
|
11
11
|
toAccountingItem,
|
|
12
12
|
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
13
|
+
import { normalizeModelProviderIds } from "@sixb/core/models"
|
|
13
14
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
14
15
|
import {
|
|
15
16
|
type AiAccountingOverview,
|
|
@@ -20,9 +21,13 @@ import {
|
|
|
20
21
|
type AiModelCallUsageRecord,
|
|
21
22
|
type ListAiModelCallAccountingInput,
|
|
22
23
|
type ListAiModelCallAccountingResult,
|
|
24
|
+
type ListAiModelCallGroupsInput,
|
|
25
|
+
type ListAiModelCallGroupsResult,
|
|
23
26
|
type QueryAiAccountingOverviewInput,
|
|
24
27
|
type SummarizeAiCostExecutionsInput,
|
|
25
28
|
} from "@sixb/core/storage"
|
|
29
|
+
import { listAiModelCallGroups } from "./ai-cost-groups"
|
|
30
|
+
import { requestedReasoningFromRow } from "./pg-ai-usage-storage"
|
|
26
31
|
import type { PgStoreClient } from "./transactions"
|
|
27
32
|
import { runPgTransaction } from "./transactions"
|
|
28
33
|
|
|
@@ -30,6 +35,12 @@ import { runPgTransaction } from "./transactions"
|
|
|
30
35
|
export class PgAiCostStorage implements AiCostStorage {
|
|
31
36
|
constructor(private readonly sql: PgStoreClient) {}
|
|
32
37
|
|
|
38
|
+
async listModelCallGroups(
|
|
39
|
+
input: ListAiModelCallGroupsInput
|
|
40
|
+
): Promise<ListAiModelCallGroupsResult> {
|
|
41
|
+
return listAiModelCallGroups(this.sql, input)
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
async recordModelCallCost(input: AiModelCallCostRecord): Promise<void> {
|
|
34
45
|
const record = normalizeAiModelCallCostRecord(input)
|
|
35
46
|
await runPgTransaction(this.sql, async (tx) => {
|
|
@@ -100,7 +111,12 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
100
111
|
usage.text_output_tokens,
|
|
101
112
|
usage.reasoning_output_tokens,
|
|
102
113
|
usage.reporting_status,
|
|
103
|
-
|
|
114
|
+
CASE
|
|
115
|
+
WHEN direct_agent.kind = 'conversation' THEN 'agent'
|
|
116
|
+
WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
|
|
117
|
+
ELSE NULL
|
|
118
|
+
END AS attribution_agent_kind,
|
|
119
|
+
workflow_node.node_id AS attribution_agent_step_id,
|
|
104
120
|
workflow_node.workflow_id AS attribution_workflow_id,
|
|
105
121
|
cost.status AS cost_status,
|
|
106
122
|
cost.currency AS cost_currency,
|
|
@@ -135,11 +151,12 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
135
151
|
SELECT 'totals'::text AS dimension,
|
|
136
152
|
NULL::text AS key_1,
|
|
137
153
|
NULL::text AS key_2,
|
|
154
|
+
NULL::text AS key_3,
|
|
138
155
|
NULL::timestamptz AS bucket_start,
|
|
139
156
|
classified.*
|
|
140
157
|
FROM classified
|
|
141
158
|
UNION ALL
|
|
142
|
-
SELECT 'series', NULL, NULL,
|
|
159
|
+
SELECT 'series', NULL, NULL, NULL,
|
|
143
160
|
CASE ${input.bucket}::text
|
|
144
161
|
WHEN 'hour' THEN
|
|
145
162
|
date_trunc('hour', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
|
|
@@ -150,17 +167,18 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
150
167
|
classified.*
|
|
151
168
|
FROM classified
|
|
152
169
|
UNION ALL
|
|
153
|
-
SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
|
|
170
|
+
SELECT 'model', provider_id, requested_model_id, NULL, NULL, classified.* FROM classified
|
|
154
171
|
UNION ALL
|
|
155
|
-
SELECT 'agent',
|
|
156
|
-
FROM classified WHERE
|
|
172
|
+
SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, classified.*
|
|
173
|
+
FROM classified WHERE attribution_agent_kind IS NOT NULL
|
|
157
174
|
UNION ALL
|
|
158
|
-
SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
|
|
175
|
+
SELECT 'workflow', attribution_workflow_id, NULL, NULL, NULL, classified.*
|
|
159
176
|
FROM classified WHERE attribution_workflow_id IS NOT NULL
|
|
160
177
|
)
|
|
161
178
|
SELECT dimension,
|
|
162
179
|
key_1,
|
|
163
180
|
key_2,
|
|
181
|
+
key_3,
|
|
164
182
|
bucket_start,
|
|
165
183
|
amount_currency,
|
|
166
184
|
COUNT(*)::text AS model_call_count,
|
|
@@ -187,7 +205,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
187
205
|
AS unvalued_call_count,
|
|
188
206
|
SUM(amount_nanos)::text AS amount_nanos
|
|
189
207
|
FROM expanded
|
|
190
|
-
GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
|
|
208
|
+
GROUP BY dimension, key_1, key_2, key_3, bucket_start, amount_currency
|
|
191
209
|
`
|
|
192
210
|
return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow))
|
|
193
211
|
}
|
|
@@ -221,13 +239,15 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
221
239
|
const rows = await this.sql<JoinedRow[]>`
|
|
222
240
|
SELECT usage.*,
|
|
223
241
|
CASE
|
|
224
|
-
WHEN direct_agent.
|
|
242
|
+
WHEN direct_agent.kind = 'conversation' THEN 'agent'
|
|
243
|
+
WHEN direct_agent.kind = 'subagent' THEN 'subagent'
|
|
225
244
|
WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
|
|
226
245
|
ELSE NULL
|
|
227
246
|
END AS attribution_kind,
|
|
228
|
-
|
|
247
|
+
workflow_node.node_id AS attribution_agent_step_id,
|
|
229
248
|
direct_agent.id AS attribution_agent_run_id,
|
|
230
249
|
direct_agent.thread_id AS attribution_thread_id,
|
|
250
|
+
direct_agent.parent_run_id AS attribution_parent_run_id,
|
|
231
251
|
workflow_agent.node_run_id AS attribution_node_run_id,
|
|
232
252
|
workflow_node.workflow_id AS attribution_workflow_id,
|
|
233
253
|
workflow_node.workflow_run_id AS attribution_workflow_run_id,
|
|
@@ -303,6 +323,7 @@ interface UsageRow {
|
|
|
303
323
|
readonly call_id: string
|
|
304
324
|
readonly provider_id: string
|
|
305
325
|
readonly requested_model_id: string
|
|
326
|
+
readonly requested_reasoning: string | Record<string, unknown> | null
|
|
306
327
|
readonly response_model_id: string | null
|
|
307
328
|
readonly response_id: string
|
|
308
329
|
readonly input_tokens: number | string | null
|
|
@@ -315,15 +336,17 @@ interface UsageRow {
|
|
|
315
336
|
readonly reasoning_output_tokens: number | string | null
|
|
316
337
|
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
317
338
|
readonly raw_usage: ReadonlyJsonObject | string | null
|
|
339
|
+
readonly provider_ids: Record<string, unknown> | string | null
|
|
318
340
|
readonly occurred_at: Date | string
|
|
319
341
|
readonly recorded_at: Date | string
|
|
320
342
|
}
|
|
321
343
|
|
|
322
344
|
interface JoinedRow extends UsageRow {
|
|
323
|
-
readonly attribution_kind: "agent" | "workflowAgent" | null
|
|
324
|
-
readonly
|
|
345
|
+
readonly attribution_kind: "agent" | "subagent" | "workflowAgent" | null
|
|
346
|
+
readonly attribution_agent_step_id: string | null
|
|
325
347
|
readonly attribution_agent_run_id: string | null
|
|
326
348
|
readonly attribution_thread_id: string | null
|
|
349
|
+
readonly attribution_parent_run_id: string | null
|
|
327
350
|
readonly attribution_node_run_id: string | null
|
|
328
351
|
readonly attribution_workflow_id: string | null
|
|
329
352
|
readonly attribution_workflow_run_id: string | null
|
|
@@ -342,6 +365,7 @@ interface AggregateRow {
|
|
|
342
365
|
readonly dimension: AiAccountingAggregateFragment["dimension"]
|
|
343
366
|
readonly key_1: string | null
|
|
344
367
|
readonly key_2: string | null
|
|
368
|
+
readonly key_3: string | null
|
|
345
369
|
readonly bucket_start: Date | string | null
|
|
346
370
|
readonly amount_currency: string | null
|
|
347
371
|
readonly model_call_count: string
|
|
@@ -405,7 +429,12 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
405
429
|
row.dimension === "model"
|
|
406
430
|
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
407
431
|
: row.dimension === "agent"
|
|
408
|
-
? {
|
|
432
|
+
? {
|
|
433
|
+
agentKind: required(row.key_1) as "agent" | "workflowAgent",
|
|
434
|
+
...(row.key_1 === "workflowAgent"
|
|
435
|
+
? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
|
|
436
|
+
: {}),
|
|
437
|
+
}
|
|
409
438
|
: row.dimension === "workflow"
|
|
410
439
|
? { workflowId: required(row.key_1) }
|
|
411
440
|
: row.dimension === "series"
|
|
@@ -466,17 +495,22 @@ function accountingItemFromRow(row: JoinedRow): AiAccountingRecordSetItem {
|
|
|
466
495
|
: row.attribution_kind === "agent"
|
|
467
496
|
? {
|
|
468
497
|
kind: "agent" as const,
|
|
469
|
-
agentId: required(row.attribution_agent_id),
|
|
470
498
|
agentRunId: required(row.attribution_agent_run_id),
|
|
471
499
|
threadId: required(row.attribution_thread_id),
|
|
472
500
|
}
|
|
473
|
-
:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
501
|
+
: row.attribution_kind === "subagent"
|
|
502
|
+
? {
|
|
503
|
+
kind: "subagent" as const,
|
|
504
|
+
subagentRunId: required(row.attribution_agent_run_id),
|
|
505
|
+
parentRunId: required(row.attribution_parent_run_id),
|
|
506
|
+
}
|
|
507
|
+
: {
|
|
508
|
+
kind: "workflowAgent" as const,
|
|
509
|
+
agentStepId: required(row.attribution_agent_step_id),
|
|
510
|
+
nodeRunId: required(row.attribution_node_run_id),
|
|
511
|
+
workflowId: required(row.attribution_workflow_id),
|
|
512
|
+
workflowRunId: required(row.attribution_workflow_run_id),
|
|
513
|
+
}
|
|
480
514
|
return {
|
|
481
515
|
usage,
|
|
482
516
|
...(attribution ? { attribution } : {}),
|
|
@@ -494,7 +528,17 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
|
|
|
494
528
|
callId: row.call_id,
|
|
495
529
|
requesterGroupIds,
|
|
496
530
|
providerId: row.provider_id,
|
|
531
|
+
...(row.provider_ids === null
|
|
532
|
+
? {}
|
|
533
|
+
: {
|
|
534
|
+
providerIds: normalizeModelProviderIds(
|
|
535
|
+
typeof row.provider_ids === "string" ? JSON.parse(row.provider_ids) : row.provider_ids
|
|
536
|
+
),
|
|
537
|
+
}),
|
|
497
538
|
requestedModelId: row.requested_model_id,
|
|
539
|
+
...(row.requested_reasoning === null
|
|
540
|
+
? {}
|
|
541
|
+
: { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
|
|
498
542
|
...(row.response_model_id === null ? {} : { responseModelId: row.response_model_id }),
|
|
499
543
|
responseId: row.response_id,
|
|
500
544
|
usage: {
|
|
@@ -569,14 +613,15 @@ function costRecord(input: {
|
|
|
569
613
|
ratedAt: Date | string
|
|
570
614
|
}): AiModelCallCostRecord {
|
|
571
615
|
const details = parseAiModelCallCostDetails(input.details)
|
|
616
|
+
const priceSource =
|
|
617
|
+
details.priceSource === undefined
|
|
618
|
+
? undefined
|
|
619
|
+
: { ...details.priceSource, observedAt: new Date(details.priceSource.observedAt) }
|
|
572
620
|
const base = {
|
|
621
|
+
...(details.estimate === undefined ? {} : { estimate: details.estimate }),
|
|
573
622
|
projectId: input.projectId,
|
|
574
623
|
usageRecordId: input.usageId,
|
|
575
624
|
pricingContext: details.pricingContext,
|
|
576
|
-
priceSource: {
|
|
577
|
-
...details.priceSource,
|
|
578
|
-
observedAt: new Date(details.priceSource.observedAt),
|
|
579
|
-
},
|
|
580
625
|
ratedAt: new Date(input.ratedAt),
|
|
581
626
|
}
|
|
582
627
|
if (input.status === "rated") {
|
|
@@ -584,6 +629,7 @@ function costRecord(input: {
|
|
|
584
629
|
...base,
|
|
585
630
|
status: "rated",
|
|
586
631
|
billingIdentity: { providerId: required(input.providerId), modelId: required(input.modelId) },
|
|
632
|
+
priceSource: required(priceSource),
|
|
587
633
|
money: { currency: required(input.currency), amountNanos: required(input.amountNanos) },
|
|
588
634
|
components: required(details.components),
|
|
589
635
|
})
|
|
@@ -594,6 +640,7 @@ function costRecord(input: {
|
|
|
594
640
|
...(input.providerId && input.modelId
|
|
595
641
|
? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } }
|
|
596
642
|
: {}),
|
|
643
|
+
...(priceSource === undefined ? {} : { priceSource }),
|
|
597
644
|
reason: required(input.reason) as Extract<
|
|
598
645
|
AiModelCallCostRecord,
|
|
599
646
|
{ status: "unpriceable" }
|