@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,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AiModelCallGroupRow,
|
|
3
|
+
aiModelCallGroupFragmentFromRow,
|
|
4
|
+
buildAiModelCallGroupsFromFragments,
|
|
5
|
+
normalizeAiModelCallAccountingQuery,
|
|
6
|
+
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
7
|
+
import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
|
|
8
|
+
import type { PgStoreClient } from "./transactions"
|
|
9
|
+
|
|
10
|
+
/** Paginate initiating executions, then aggregate their matching calls without loading usage payloads. */
|
|
11
|
+
export async function listAiModelCallGroups(
|
|
12
|
+
sql: PgStoreClient,
|
|
13
|
+
input: ListAiModelCallGroupsInput
|
|
14
|
+
): Promise<ListAiModelCallGroupsResult> {
|
|
15
|
+
const query = normalizeAiModelCallAccountingQuery(input)
|
|
16
|
+
const parameters = [
|
|
17
|
+
query.projectId,
|
|
18
|
+
query.from.toISOString(),
|
|
19
|
+
query.to.toISOString(),
|
|
20
|
+
query.providerId ?? null,
|
|
21
|
+
query.modelId ?? null,
|
|
22
|
+
query.valuationStatus ?? null,
|
|
23
|
+
]
|
|
24
|
+
const [count] = await sql.unsafe<{ total: string }[]>(COUNT_SQL, parameters)
|
|
25
|
+
const rows = await sql.unsafe<AiModelCallGroupRow[]>(PAGE_SQL, [
|
|
26
|
+
...parameters,
|
|
27
|
+
query.limit,
|
|
28
|
+
query.offset,
|
|
29
|
+
])
|
|
30
|
+
const total = Number(count?.total)
|
|
31
|
+
if (!Number.isSafeInteger(total) || total < 0)
|
|
32
|
+
throw new RangeError("[Sixb] Invalid model-call group count.")
|
|
33
|
+
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow))
|
|
34
|
+
return { items, total, hasMore: query.offset + items.length < total }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const FILTERED_SQL = `
|
|
38
|
+
WITH filtered AS (
|
|
39
|
+
SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
|
|
40
|
+
usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
|
|
41
|
+
usage.total_tokens,
|
|
42
|
+
COALESCE(cost.status, 'unvalued') AS valuation_status,
|
|
43
|
+
cost.currency AS amount_currency, cost.amount_nanos,
|
|
44
|
+
root_agent.id AS root_agent_run_id,
|
|
45
|
+
root_agent.thread_id AS root_thread_id,
|
|
46
|
+
direct_agent.kind AS direct_kind,
|
|
47
|
+
direct_agent.id AS direct_run_id,
|
|
48
|
+
direct_agent.parent_run_id AS parent_run_id,
|
|
49
|
+
workflow_node.node_id AS workflow_agent_step_id,
|
|
50
|
+
workflow_agent.node_run_id AS workflow_node_run_id,
|
|
51
|
+
workflow_node.workflow_id AS workflow_id,
|
|
52
|
+
workflow_node.workflow_run_id AS workflow_run_id
|
|
53
|
+
FROM ai_model_call_usage AS usage
|
|
54
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
55
|
+
ON direct_agent.project_id = usage.project_id AND direct_agent.execution_id = usage.execution_id
|
|
56
|
+
LEFT JOIN agent_runs AS root_agent
|
|
57
|
+
ON root_agent.project_id = usage.project_id
|
|
58
|
+
AND root_agent.id = COALESCE(direct_agent.parent_run_id, direct_agent.id)
|
|
59
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
60
|
+
ON workflow_agent.project_id = usage.project_id AND workflow_agent.execution_id = usage.execution_id
|
|
61
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
62
|
+
ON workflow_node.project_id = workflow_agent.project_id AND workflow_node.id = workflow_agent.node_run_id
|
|
63
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
64
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
65
|
+
WHERE usage.project_id = $1
|
|
66
|
+
AND usage.occurred_at >= $2::timestamptz
|
|
67
|
+
AND usage.occurred_at < $3::timestamptz
|
|
68
|
+
AND ($4::text IS NULL OR usage.provider_id = $4)
|
|
69
|
+
AND ($5::text IS NULL OR usage.requested_model_id = $5)
|
|
70
|
+
AND ($6::text IS NULL OR COALESCE(cost.status, 'unvalued') = $6)
|
|
71
|
+
), roots AS (
|
|
72
|
+
SELECT root_execution_id, MAX(occurred_at) AS last_call_at
|
|
73
|
+
FROM filtered GROUP BY root_execution_id
|
|
74
|
+
)
|
|
75
|
+
`
|
|
76
|
+
const COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`
|
|
77
|
+
const PAGE_SQL = `${FILTERED_SQL}, page AS (
|
|
78
|
+
SELECT root_execution_id FROM roots
|
|
79
|
+
ORDER BY last_call_at DESC, root_execution_id COLLATE "C" ASC LIMIT $7 OFFSET $8
|
|
80
|
+
)
|
|
81
|
+
SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
82
|
+
MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
|
|
83
|
+
CAST(COUNT(*) AS TEXT) AS call_count,
|
|
84
|
+
CAST(COUNT(total_tokens) AS TEXT) AS token_count,
|
|
85
|
+
CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
|
|
86
|
+
valuation_status, amount_currency,
|
|
87
|
+
CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
|
|
88
|
+
CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
|
|
89
|
+
CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low,
|
|
90
|
+
MAX(root_agent_run_id) AS root_agent_run_id,
|
|
91
|
+
MAX(root_thread_id) AS root_thread_id,
|
|
92
|
+
MAX(direct_kind) AS direct_kind,
|
|
93
|
+
MAX(direct_run_id) AS direct_run_id,
|
|
94
|
+
MAX(parent_run_id) AS parent_run_id,
|
|
95
|
+
MAX(workflow_agent_step_id) AS workflow_agent_step_id,
|
|
96
|
+
MAX(workflow_node_run_id) AS workflow_node_run_id,
|
|
97
|
+
MAX(workflow_id) AS workflow_id,
|
|
98
|
+
MAX(workflow_run_id) AS workflow_run_id
|
|
99
|
+
FROM filtered JOIN page ON page.root_execution_id = filtered.root_execution_id
|
|
100
|
+
GROUP BY filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
101
|
+
valuation_status, amount_currency
|
|
102
|
+
`
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AuthStorage,
|
|
3
|
+
CompleteDeviceAuthorizationInput,
|
|
4
|
+
CompleteDeviceAuthorizationResult,
|
|
3
5
|
CompleteMagicLinkSignInInput,
|
|
4
6
|
CompleteOidcSignInInput,
|
|
5
7
|
CompleteSignInResult,
|
|
8
|
+
DeviceAuthorizationRecord,
|
|
6
9
|
GroupMembershipRecord,
|
|
7
10
|
InvitationRecord,
|
|
8
11
|
SessionRecord,
|
|
@@ -19,6 +22,10 @@ import {
|
|
|
19
22
|
runPgTransaction,
|
|
20
23
|
} from "../transactions"
|
|
21
24
|
import { PgAuthAccessTokenStore } from "./access-tokens"
|
|
25
|
+
import {
|
|
26
|
+
getPgDeviceAuthorizationById,
|
|
27
|
+
PgAuthDeviceAuthorizationStore,
|
|
28
|
+
} from "./device-authorizations"
|
|
22
29
|
import { PgAuthGroupMembershipStore } from "./group-memberships"
|
|
23
30
|
import { PgAuthUserIdentityStore } from "./identities"
|
|
24
31
|
import { PgAuthInvitationStore } from "./invitations"
|
|
@@ -77,6 +84,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
77
84
|
readonly groupMemberships: PgAuthGroupMembershipStore
|
|
78
85
|
readonly magicLinks: PgAuthMagicLinkStore
|
|
79
86
|
readonly oidcAuthorizationAttempts: PgAuthOidcAuthorizationAttemptStore
|
|
87
|
+
readonly deviceAuthorizations: PgAuthDeviceAuthorizationStore
|
|
80
88
|
|
|
81
89
|
constructor(options: PgAuthStorageOptions) {
|
|
82
90
|
this.sql = options.sql
|
|
@@ -90,6 +98,43 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
90
98
|
this.groupMemberships = new PgAuthGroupMembershipStore(this.sql)
|
|
91
99
|
this.magicLinks = new PgAuthMagicLinkStore(this.sql)
|
|
92
100
|
this.oidcAuthorizationAttempts = new PgAuthOidcAuthorizationAttemptStore(this.sql)
|
|
101
|
+
this.deviceAuthorizations = new PgAuthDeviceAuthorizationStore(this.sql)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async completeDeviceAuthorization(
|
|
105
|
+
input: CompleteDeviceAuthorizationInput
|
|
106
|
+
): Promise<CompleteDeviceAuthorizationResult> {
|
|
107
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
108
|
+
const authorization = await getPgDeviceAuthorizationById(tx, input.projectId, input.id)
|
|
109
|
+
if (!authorization) {
|
|
110
|
+
throw new AuthStorageError(
|
|
111
|
+
"missing_device_authorization",
|
|
112
|
+
`[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
assertCompletableDeviceAuthorization(authorization, input)
|
|
116
|
+
const accessToken = await new PgAuthAccessTokenStore(tx).create(input.accessToken)
|
|
117
|
+
const [consumed] = await tx`
|
|
118
|
+
UPDATE auth_device_authorizations
|
|
119
|
+
SET status = 'consumed', consumed_at = ${input.completedAt}
|
|
120
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = 'approved'
|
|
121
|
+
RETURNING *
|
|
122
|
+
`
|
|
123
|
+
if (!consumed) {
|
|
124
|
+
throw new AuthStorageError(
|
|
125
|
+
"invalid_device_authorization",
|
|
126
|
+
"[Sixb] Device authorization was already consumed."
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
authorization: {
|
|
131
|
+
...authorization,
|
|
132
|
+
status: "consumed" as const,
|
|
133
|
+
consumedAt: new Date(input.completedAt),
|
|
134
|
+
},
|
|
135
|
+
accessToken,
|
|
136
|
+
}
|
|
137
|
+
})
|
|
93
138
|
}
|
|
94
139
|
|
|
95
140
|
async completeMagicLinkSignIn(
|
|
@@ -796,3 +841,42 @@ function assertSignInSessionAudience(
|
|
|
796
841
|
`[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
|
|
797
842
|
)
|
|
798
843
|
}
|
|
844
|
+
|
|
845
|
+
function assertCompletableDeviceAuthorization(
|
|
846
|
+
authorization: DeviceAuthorizationRecord,
|
|
847
|
+
input: CompleteDeviceAuthorizationInput
|
|
848
|
+
): void {
|
|
849
|
+
if (authorization.deviceCodeHash !== input.deviceCodeHash) {
|
|
850
|
+
throw new AuthStorageError(
|
|
851
|
+
"invalid_device_authorization",
|
|
852
|
+
"[Sixb] Invalid device authorization."
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
if (authorization.status === "pending") {
|
|
856
|
+
throw new AuthStorageError(
|
|
857
|
+
"pending_device_authorization",
|
|
858
|
+
"[Sixb] Device authorization is pending."
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
if (authorization.status === "denied") {
|
|
862
|
+
throw new AuthStorageError(
|
|
863
|
+
"device_authorization_denied",
|
|
864
|
+
"[Sixb] Device authorization was denied."
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
if (
|
|
868
|
+
authorization.status !== "approved" ||
|
|
869
|
+
authorization.expiresAt <= input.completedAt ||
|
|
870
|
+
!authorization.approvedUserId ||
|
|
871
|
+
!authorization.approvedSessionId ||
|
|
872
|
+
input.accessToken.projectId !== input.projectId ||
|
|
873
|
+
input.accessToken.subjectType !== "user" ||
|
|
874
|
+
input.accessToken.subjectId !== authorization.approvedUserId ||
|
|
875
|
+
input.accessToken.createdBySessionId !== authorization.approvedSessionId
|
|
876
|
+
) {
|
|
877
|
+
throw new AuthStorageError(
|
|
878
|
+
"invalid_device_authorization",
|
|
879
|
+
"[Sixb] Device authorization is invalid."
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthDeviceAuthorizationStore,
|
|
3
|
+
CreateDeviceAuthorizationInput,
|
|
4
|
+
DeviceAuthorizationRecord,
|
|
5
|
+
} from "@sixb/core/storage"
|
|
6
|
+
import { AuthStorageError, MAX_PENDING_DEVICE_AUTHORIZATIONS } from "@sixb/core/storage"
|
|
7
|
+
import {
|
|
8
|
+
authLockKey,
|
|
9
|
+
lockAdvisoryKeys,
|
|
10
|
+
type PgStoreClient,
|
|
11
|
+
runPgTransaction,
|
|
12
|
+
} from "../transactions"
|
|
13
|
+
import type { PgAuthDeviceAuthorizationRow } from "./rows"
|
|
14
|
+
import { rowToDeviceAuthorizationRecord } from "./rows"
|
|
15
|
+
import { assertNonEmpty, mapUniqueConstraintError } from "./shared"
|
|
16
|
+
|
|
17
|
+
export class PgAuthDeviceAuthorizationStore implements AuthDeviceAuthorizationStore {
|
|
18
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
19
|
+
|
|
20
|
+
async create(input: CreateDeviceAuthorizationInput): Promise<DeviceAuthorizationRecord> {
|
|
21
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
22
|
+
await lockAdvisoryKeys(tx, [authLockKey("device-authorizations", input.projectId)])
|
|
23
|
+
await tx`
|
|
24
|
+
DELETE FROM auth_device_authorizations
|
|
25
|
+
WHERE project_id = ${input.projectId} AND expires_at <= ${input.createdAt}
|
|
26
|
+
`
|
|
27
|
+
const [pending] = await tx<{ readonly count: string | number }[]>`
|
|
28
|
+
SELECT COUNT(*)::bigint AS count FROM auth_device_authorizations
|
|
29
|
+
WHERE project_id = ${input.projectId} AND status = 'pending'
|
|
30
|
+
`
|
|
31
|
+
if (Number(pending?.count ?? 0) >= MAX_PENDING_DEVICE_AUTHORIZATIONS) {
|
|
32
|
+
throw new AuthStorageError(
|
|
33
|
+
"device_authorization_limit_exceeded",
|
|
34
|
+
`[Sixb] Project '${input.projectId}' has too many pending device authorizations.`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const [row] = await tx<PgAuthDeviceAuthorizationRow[]>`
|
|
39
|
+
INSERT INTO auth_device_authorizations (
|
|
40
|
+
project_id, id, device_code_hash, user_code, client_name, token_name,
|
|
41
|
+
token_expires_at, status, created_at, expires_at
|
|
42
|
+
) VALUES (
|
|
43
|
+
${assertNonEmpty(input.projectId, "Project id")},
|
|
44
|
+
${assertNonEmpty(input.id, "Device authorization id")},
|
|
45
|
+
${assertNonEmpty(input.deviceCodeHash, "Device code hash")},
|
|
46
|
+
${assertNonEmpty(input.userCode, "User code")},
|
|
47
|
+
${assertNonEmpty(input.clientName, "Client name")},
|
|
48
|
+
${assertNonEmpty(input.tokenName, "Token name")},
|
|
49
|
+
${input.tokenExpiresAt}, 'pending', ${input.createdAt}, ${input.expiresAt}
|
|
50
|
+
) RETURNING *
|
|
51
|
+
`
|
|
52
|
+
return rowToDeviceAuthorizationRecord(row)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
mapUniqueConstraintError(
|
|
55
|
+
error,
|
|
56
|
+
"duplicate_device_authorization",
|
|
57
|
+
`[Sixb] Device authorization '${input.id}' already exists for project '${input.projectId}'.`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getById(params: { readonly projectId: string; readonly id: string }) {
|
|
64
|
+
return getPgDeviceAuthorizationById(this.sql, params.projectId, params.id)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getByUserCode(params: { readonly projectId: string; readonly userCode: string }) {
|
|
68
|
+
const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
|
|
69
|
+
SELECT * FROM auth_device_authorizations
|
|
70
|
+
WHERE project_id = ${params.projectId} AND user_code = ${params.userCode}
|
|
71
|
+
`
|
|
72
|
+
return row ? rowToDeviceAuthorizationRecord(row) : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async approve(params: {
|
|
76
|
+
readonly projectId: string
|
|
77
|
+
readonly id: string
|
|
78
|
+
readonly userId: string
|
|
79
|
+
readonly sessionId: string
|
|
80
|
+
readonly approvedAt: Date
|
|
81
|
+
}) {
|
|
82
|
+
const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
|
|
83
|
+
UPDATE auth_device_authorizations
|
|
84
|
+
SET status = 'approved', approved_user_id = ${params.userId},
|
|
85
|
+
approved_session_id = ${params.sessionId}, approved_at = ${params.approvedAt}
|
|
86
|
+
WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
87
|
+
AND status = 'pending' AND expires_at > ${params.approvedAt}
|
|
88
|
+
AND EXISTS (
|
|
89
|
+
SELECT 1 FROM auth_sessions
|
|
90
|
+
WHERE project_id = ${params.projectId} AND id = ${params.sessionId}
|
|
91
|
+
AND user_id = ${params.userId} AND revoked_at IS NULL
|
|
92
|
+
AND expires_at > ${params.approvedAt}
|
|
93
|
+
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.approvedAt})
|
|
94
|
+
)
|
|
95
|
+
RETURNING *
|
|
96
|
+
`
|
|
97
|
+
if (!row) throw invalidTransition(params.id, params.projectId)
|
|
98
|
+
return rowToDeviceAuthorizationRecord(row)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async deny(params: { readonly projectId: string; readonly id: string; readonly deniedAt: Date }) {
|
|
102
|
+
const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
|
|
103
|
+
UPDATE auth_device_authorizations
|
|
104
|
+
SET status = 'denied', denied_at = ${params.deniedAt}
|
|
105
|
+
WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
106
|
+
AND status = 'pending' AND expires_at > ${params.deniedAt}
|
|
107
|
+
RETURNING *
|
|
108
|
+
`
|
|
109
|
+
if (!row) throw invalidTransition(params.id, params.projectId)
|
|
110
|
+
return rowToDeviceAuthorizationRecord(row)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getPgDeviceAuthorizationById(
|
|
115
|
+
sql: PgStoreClient,
|
|
116
|
+
projectId: string,
|
|
117
|
+
id: string
|
|
118
|
+
) {
|
|
119
|
+
const [row] = await sql<PgAuthDeviceAuthorizationRow[]>`
|
|
120
|
+
SELECT * FROM auth_device_authorizations
|
|
121
|
+
WHERE project_id = ${projectId} AND id = ${id}
|
|
122
|
+
`
|
|
123
|
+
return row ? rowToDeviceAuthorizationRecord(row) : null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function invalidTransition(id: string, projectId: string): AuthStorageError {
|
|
127
|
+
return new AuthStorageError(
|
|
128
|
+
"invalid_device_authorization",
|
|
129
|
+
`[Sixb] Device authorization '${id}' is no longer pending for project '${projectId}'.`
|
|
130
|
+
)
|
|
131
|
+
}
|
package/src/auth-storage/rows.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Principal } from "@sixb/core"
|
|
2
2
|
import type {
|
|
3
3
|
AccessTokenRecord,
|
|
4
|
+
DeviceAuthorizationRecord,
|
|
4
5
|
GroupMembershipRecord,
|
|
5
6
|
GroupMembershipSource,
|
|
6
7
|
InvitationRecord,
|
|
@@ -150,6 +151,24 @@ export interface PgAuthOidcAttemptRow {
|
|
|
150
151
|
readonly consumed_at: PgDate | null
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
export interface PgAuthDeviceAuthorizationRow {
|
|
155
|
+
readonly project_id: string
|
|
156
|
+
readonly id: string
|
|
157
|
+
readonly device_code_hash: string
|
|
158
|
+
readonly user_code: string
|
|
159
|
+
readonly client_name: string
|
|
160
|
+
readonly token_name: string
|
|
161
|
+
readonly token_expires_at: PgDate
|
|
162
|
+
readonly status: DeviceAuthorizationRecord["status"]
|
|
163
|
+
readonly approved_user_id: string | null
|
|
164
|
+
readonly approved_session_id: string | null
|
|
165
|
+
readonly created_at: PgDate
|
|
166
|
+
readonly expires_at: PgDate
|
|
167
|
+
readonly approved_at: PgDate | null
|
|
168
|
+
readonly denied_at: PgDate | null
|
|
169
|
+
readonly consumed_at: PgDate | null
|
|
170
|
+
}
|
|
171
|
+
|
|
153
172
|
export function rowToUserRecord(row: PgAuthUserRow): UserRecord {
|
|
154
173
|
return {
|
|
155
174
|
id: row.id,
|
|
@@ -304,6 +323,28 @@ export function rowToOidcAuthorizationAttemptRecord(
|
|
|
304
323
|
}
|
|
305
324
|
}
|
|
306
325
|
|
|
326
|
+
export function rowToDeviceAuthorizationRecord(
|
|
327
|
+
row: PgAuthDeviceAuthorizationRow
|
|
328
|
+
): DeviceAuthorizationRecord {
|
|
329
|
+
return {
|
|
330
|
+
id: row.id,
|
|
331
|
+
projectId: row.project_id,
|
|
332
|
+
deviceCodeHash: row.device_code_hash,
|
|
333
|
+
userCode: row.user_code,
|
|
334
|
+
clientName: row.client_name,
|
|
335
|
+
tokenName: row.token_name,
|
|
336
|
+
tokenExpiresAt: toDate(row.token_expires_at),
|
|
337
|
+
status: row.status,
|
|
338
|
+
approvedUserId: row.approved_user_id ?? undefined,
|
|
339
|
+
approvedSessionId: row.approved_session_id ?? undefined,
|
|
340
|
+
createdAt: toDate(row.created_at),
|
|
341
|
+
expiresAt: toDate(row.expires_at),
|
|
342
|
+
approvedAt: row.approved_at ? toDate(row.approved_at) : undefined,
|
|
343
|
+
deniedAt: row.denied_at ? toDate(row.denied_at) : undefined,
|
|
344
|
+
consumedAt: row.consumed_at ? toDate(row.consumed_at) : undefined,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
307
348
|
export function serializeOptionalRecord(
|
|
308
349
|
value: Readonly<Record<string, unknown>> | undefined
|
|
309
350
|
): string | null {
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "@sixb/core/internal/storage-operation-scope"
|
|
21
21
|
import {
|
|
22
22
|
type AiCostStorage,
|
|
23
|
+
type AiLimitStorage,
|
|
23
24
|
type AiUsageStorage,
|
|
24
25
|
createTransactionStorageProxy,
|
|
25
26
|
type ObjectStorage,
|
|
@@ -33,6 +34,7 @@ import { createPostgresStorageMigrators, dropSchema } from "./migrations"
|
|
|
33
34
|
import { PgOntologyStorage, type PgOntologyTransactionContext } from "./ontology-storage"
|
|
34
35
|
import { PgActionRunStorage } from "./pg-action-run-storage"
|
|
35
36
|
import { PgAiCostStorage } from "./pg-ai-cost-storage"
|
|
37
|
+
import { PgAiLimitStorage } from "./pg-ai-limit-storage"
|
|
36
38
|
import { PgAiUsageStorage } from "./pg-ai-usage-storage"
|
|
37
39
|
import { createPgClient, type SQL, type SQLClient } from "./pg-client"
|
|
38
40
|
import { PgExecutionStorage } from "./pg-execution-storage"
|
|
@@ -138,6 +140,7 @@ export class PostgresStorage implements MigrationCapableStorage {
|
|
|
138
140
|
readonly agents: PgAgentStorage
|
|
139
141
|
readonly aiUsage: AiUsageStorage
|
|
140
142
|
readonly aiCosts: AiCostStorage
|
|
143
|
+
readonly aiLimits: AiLimitStorage
|
|
141
144
|
readonly actionRuns: PgActionRunStorage
|
|
142
145
|
readonly pipelineRuns: PgPipelineRunStorage
|
|
143
146
|
readonly workflowRuns: PgWorkflowRunStorage
|
|
@@ -212,6 +215,7 @@ export class PostgresStorage implements MigrationCapableStorage {
|
|
|
212
215
|
this.agents = createAgentOperationScope(stores.agents, scope)
|
|
213
216
|
this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
|
|
214
217
|
this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
|
|
218
|
+
this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
|
|
215
219
|
this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope)
|
|
216
220
|
this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope)
|
|
217
221
|
this.workflowRuns = createWorkflowRunOperationScope(stores.workflowRuns, scope)
|
|
@@ -258,6 +262,7 @@ export class PostgresStorage implements MigrationCapableStorage {
|
|
|
258
262
|
transactionContext.materializations.assertCommittable()
|
|
259
263
|
return result
|
|
260
264
|
} finally {
|
|
265
|
+
await txStorage.aiLimits.settleOperations()
|
|
261
266
|
transactionContext.active = false
|
|
262
267
|
txStorage.ontology.deactivateSessions()
|
|
263
268
|
transactionContext.materializations.deactivate()
|
|
@@ -305,7 +310,7 @@ export class PostgresStorage implements MigrationCapableStorage {
|
|
|
305
310
|
private createTransactionStorage(
|
|
306
311
|
client: PgStoreClient,
|
|
307
312
|
transactionContext: PgOntologyTransactionContext
|
|
308
|
-
): Storage & { readonly ontology: PgOntologyStorage } {
|
|
313
|
+
): Storage & { readonly ontology: PgOntologyStorage; readonly aiLimits: PgAiLimitStorage } {
|
|
309
314
|
return {
|
|
310
315
|
...createPostgresStores(client, {
|
|
311
316
|
runOntologyOperation: async (run) => run(client),
|
|
@@ -342,6 +347,7 @@ function createPostgresStores(
|
|
|
342
347
|
agents: new PgAgentStorage({ sql, executions }),
|
|
343
348
|
aiUsage: new PgAiUsageStorage(sql),
|
|
344
349
|
aiCosts: new PgAiCostStorage(sql),
|
|
350
|
+
aiLimits: new PgAiLimitStorage(sql),
|
|
345
351
|
actionRuns: new PgActionRunStorage(sql, executions),
|
|
346
352
|
pipelineRuns: new PgPipelineRunStorage(sql, executions),
|
|
347
353
|
workflowRuns: new PgWorkflowRunStorage(sql, executions),
|
|
@@ -363,6 +369,7 @@ interface PostgresStoreSet {
|
|
|
363
369
|
readonly agents: PgAgentStorage
|
|
364
370
|
readonly aiUsage: PgAiUsageStorage
|
|
365
371
|
readonly aiCosts: PgAiCostStorage
|
|
372
|
+
readonly aiLimits: PgAiLimitStorage
|
|
366
373
|
readonly actionRuns: PgActionRunStorage
|
|
367
374
|
readonly pipelineRuns: PgPipelineRunStorage
|
|
368
375
|
readonly workflowRuns: PgWorkflowRunStorage
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Call-time model configuration and provider identity. Historical identity remains unknown.
|
|
2
|
+
ALTER TABLE ai_model_call_usage
|
|
3
|
+
ADD COLUMN requested_reasoning JSONB
|
|
4
|
+
CHECK (
|
|
5
|
+
requested_reasoning IS NULL OR
|
|
6
|
+
jsonb_typeof(requested_reasoning) IN ('string', 'object')
|
|
7
|
+
);
|
|
8
|
+
ALTER TABLE ai_model_call_usage ADD COLUMN provider_ids JSONB;
|
|
9
|
+
|
|
10
|
+
-- Upgrade the shipped valuation reason while retaining the selected-cost projection.
|
|
11
|
+
ALTER TABLE ai_model_call_valuations
|
|
12
|
+
DROP CONSTRAINT ai_model_call_valuations_reason_check;
|
|
13
|
+
|
|
14
|
+
UPDATE ai_model_call_valuations SET reason = 'missingRateCard'
|
|
15
|
+
WHERE reason = 'missingCatalogEntry';
|
|
16
|
+
|
|
17
|
+
ALTER TABLE ai_model_call_valuations ADD CONSTRAINT ai_model_call_valuations_reason_check
|
|
18
|
+
CHECK (reason IS NULL OR reason IN (
|
|
19
|
+
'missingBillingIdentity',
|
|
20
|
+
'missingRateCard',
|
|
21
|
+
'missingUsageMeter',
|
|
22
|
+
'unsupportedPricingDimension',
|
|
23
|
+
'invalidUsageForFormula'
|
|
24
|
+
));
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
CREATE TABLE ai_usage_limits (
|
|
2
|
+
project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
|
|
3
|
+
id TEXT NOT NULL CHECK (length(btrim(id)) > 0),
|
|
4
|
+
subject_type TEXT NOT NULL CHECK (
|
|
5
|
+
subject_type IN ('project', 'group', 'user', 'serviceAccount')
|
|
6
|
+
),
|
|
7
|
+
subject_id TEXT NOT NULL CHECK (
|
|
8
|
+
(subject_type = 'project' AND subject_id = '')
|
|
9
|
+
OR (subject_type <> 'project' AND length(btrim(subject_id)) > 0)
|
|
10
|
+
),
|
|
11
|
+
meter TEXT NOT NULL CHECK (meter IN ('tokens.total', 'cost.catalogEstimated')),
|
|
12
|
+
currency TEXT NOT NULL CHECK (
|
|
13
|
+
(meter = 'tokens.total' AND currency = '')
|
|
14
|
+
OR (meter = 'cost.catalogEstimated' AND currency = 'USD')
|
|
15
|
+
),
|
|
16
|
+
limit_amount NUMERIC(78, 0) NOT NULL CHECK (limit_amount >= 0),
|
|
17
|
+
period_kind TEXT NOT NULL CHECK (period_kind = 'calendarMonth'),
|
|
18
|
+
enabled BOOLEAN NOT NULL,
|
|
19
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
20
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
21
|
+
PRIMARY KEY (project_id, id),
|
|
22
|
+
UNIQUE (project_id, subject_type, subject_id, meter, currency)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX idx_ai_usage_limits_project_enabled
|
|
26
|
+
ON ai_usage_limits (project_id, enabled, subject_type, subject_id, meter, currency);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE ai_usage_limit_periods (
|
|
29
|
+
project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
|
|
30
|
+
subject_type TEXT NOT NULL CHECK (
|
|
31
|
+
subject_type IN ('project', 'group', 'user', 'serviceAccount')
|
|
32
|
+
),
|
|
33
|
+
subject_id TEXT NOT NULL CHECK (
|
|
34
|
+
(subject_type = 'project' AND subject_id = '')
|
|
35
|
+
OR (subject_type <> 'project' AND length(btrim(subject_id)) > 0)
|
|
36
|
+
),
|
|
37
|
+
meter TEXT NOT NULL CHECK (meter IN ('tokens.total', 'cost.catalogEstimated')),
|
|
38
|
+
currency TEXT NOT NULL CHECK (
|
|
39
|
+
(meter = 'tokens.total' AND currency = '')
|
|
40
|
+
OR (meter = 'cost.catalogEstimated' AND currency = 'USD')
|
|
41
|
+
),
|
|
42
|
+
period_start TIMESTAMPTZ NOT NULL,
|
|
43
|
+
period_end TIMESTAMPTZ NOT NULL,
|
|
44
|
+
actual_amount NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK (actual_amount >= 0),
|
|
45
|
+
reserved_amount NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK (reserved_amount >= 0),
|
|
46
|
+
unknown_amount NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK (unknown_amount >= 0),
|
|
47
|
+
accounting_status TEXT NOT NULL DEFAULT 'complete' CHECK (
|
|
48
|
+
accounting_status IN ('complete', 'unavailable')
|
|
49
|
+
),
|
|
50
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
51
|
+
PRIMARY KEY (
|
|
52
|
+
project_id, subject_type, subject_id, meter, currency, period_start
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX idx_ai_usage_limit_periods_subject_period
|
|
57
|
+
ON ai_usage_limit_periods (
|
|
58
|
+
project_id, subject_type, subject_id, period_start, period_end, meter, currency
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE ai_model_call_reservations (
|
|
62
|
+
project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
|
|
63
|
+
execution_id TEXT NOT NULL CHECK (length(btrim(execution_id)) > 0),
|
|
64
|
+
attempt BIGINT NOT NULL CHECK (attempt >= 1),
|
|
65
|
+
call_id TEXT NOT NULL CHECK (length(btrim(call_id)) > 0),
|
|
66
|
+
buckets JSONB NOT NULL CHECK (jsonb_typeof(buckets) = 'array'),
|
|
67
|
+
request_key TEXT NOT NULL,
|
|
68
|
+
period_start TIMESTAMPTZ NOT NULL,
|
|
69
|
+
period_end TIMESTAMPTZ NOT NULL,
|
|
70
|
+
state TEXT NOT NULL CHECK (state IN ('active', 'reconciled', 'unknown')),
|
|
71
|
+
usage_record_id TEXT,
|
|
72
|
+
reserved_at TIMESTAMPTZ NOT NULL,
|
|
73
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
74
|
+
PRIMARY KEY (project_id, execution_id, attempt, call_id),
|
|
75
|
+
FOREIGN KEY (project_id, execution_id)
|
|
76
|
+
REFERENCES executions(project_id, id)
|
|
77
|
+
ON DELETE RESTRICT,
|
|
78
|
+
FOREIGN KEY (project_id, usage_record_id)
|
|
79
|
+
REFERENCES ai_model_call_usage(project_id, id)
|
|
80
|
+
ON DELETE RESTRICT,
|
|
81
|
+
CHECK (
|
|
82
|
+
(
|
|
83
|
+
state = 'reconciled'
|
|
84
|
+
AND usage_record_id IS NOT NULL
|
|
85
|
+
)
|
|
86
|
+
OR (
|
|
87
|
+
state <> 'reconciled'
|
|
88
|
+
AND usage_record_id IS NULL
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
CREATE INDEX idx_ai_model_call_reservations_active
|
|
94
|
+
ON ai_model_call_reservations (project_id, state, period_end, execution_id, attempt, call_id);
|