@sixb/pg 0.1.1 → 0.1.3
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 +7 -6
- package/dist/agents/agent-storage.d.ts +7 -4
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/checkpoints.d.ts +12 -0
- package/dist/agents/checkpoints.d.ts.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/messages.d.ts +2 -1
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +22 -12
- 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/auth-storage/service-accounts.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +0 -4
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/connector-connection-storage.d.ts +9 -0
- package/dist/connector-connection-storage.d.ts.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5010 -1837
- package/dist/index.js.map +31 -26
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +0 -2
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- 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/outbox.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +3 -6
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +3 -2
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts +12 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -0
- package/dist/pg-ai-usage-storage.d.ts +16 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +14 -0
- package/dist/pg-execution-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +5 -2
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
- package/dist/pg-projection-run-storage.d.ts +5 -2
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/pg-sync-run-storage.d.ts +5 -2
- package/dist/pg-sync-run-storage.d.ts.map +1 -1
- package/dist/pg-webhook-run-storage.d.ts +5 -2
- package/dist/pg-webhook-run-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +5 -3
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/run-list-query.d.ts +1 -1
- package/dist/run-list-query.d.ts.map +1 -1
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +9 -5
- package/src/agents/checkpoints.ts +175 -0
- package/src/agents/index.ts +1 -0
- package/src/agents/messages.ts +71 -13
- package/src/agents/rows.ts +57 -43
- package/src/agents/runs.ts +29 -15
- package/src/auth-storage/service-accounts.ts +14 -15
- package/src/auth-storage/shared.ts +0 -8
- package/src/connector-connection-storage.ts +760 -0
- package/src/index.ts +38 -15
- package/src/latest-run-query.ts +7 -1
- package/src/migrations/004-executions.sql +140 -0
- package/src/migrations/005-workflow-executions.sql +10 -0
- package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
- package/src/migrations/007-split-overrides.sql +153 -0
- package/src/migrations/008-action-executions.sql +7 -0
- package/src/migrations/009-agent-executions.sql +21 -0
- package/src/migrations/010-ai-usage-accounting-foundation.sql +76 -0
- package/src/migrations/011-sync-failure-record.sql +23 -0
- package/src/migrations/012-pipeline-failure-record.sql +47 -0
- package/src/migrations/013-workflow-failure-record.sql +45 -0
- package/src/migrations/014-agent-failure-record.sql +56 -0
- package/src/migrations/015-projection-failure-record.sql +20 -0
- package/src/migrations/016-webhook-run-failure-record.sql +19 -0
- package/src/migrations/017-action-failure-record.sql +65 -0
- package/src/migrations/018-ontology-outbox-failure-record.sql +29 -0
- package/src/migrations/019-webhook-delivery-failure-record.sql +27 -0
- package/src/migrations/020-drop-run-usage-projections.sql +8 -0
- package/src/migrations/021-sync-pipeline-executions.sql +50 -0
- package/src/migrations/022-projection-executions.sql +115 -0
- package/src/migrations/023-webhook-executions.sql +30 -0
- package/src/migrations/024-ontology-commit-executions.sql +12 -0
- package/src/migrations/025-connector-connections.sql +231 -0
- package/src/migrations/026-ai-cost-accounting.sql +53 -0
- package/src/migrations/027-agent-context-checkpoints.sql +34 -0
- package/src/migrations/028-object-override-edit-times.sql +3 -0
- package/src/migrations.ts +82 -0
- package/src/ontology-storage/materialization-session.ts +25 -28
- package/src/ontology-storage/materialization-state.ts +161 -43
- package/src/ontology-storage/materialization-writer.ts +149 -76
- package/src/ontology-storage/materializations.ts +3 -1
- package/src/ontology-storage/outbox.ts +9 -2
- package/src/ontology-storage/shared.ts +10 -20
- package/src/pg-action-run-storage.ts +43 -67
- package/src/pg-ai-cost-storage.ts +707 -0
- package/src/pg-ai-usage-storage.ts +290 -0
- package/src/pg-execution-storage.ts +147 -0
- package/src/pg-pipeline-run-storage.ts +107 -41
- package/src/pg-projection-run-storage.ts +159 -91
- package/src/pg-sync-run-storage.ts +100 -28
- package/src/pg-webhook-run-storage.ts +89 -57
- package/src/pg-workflow-run-storage.ts +97 -130
- package/src/run-list-query.ts +9 -4
- package/src/testing.ts +18 -0
- package/dist/pg-webhook-delivery-storage.d.ts +0 -17
- package/dist/pg-webhook-delivery-storage.d.ts.map +0 -1
- package/src/pg-webhook-delivery-storage.ts +0 -180
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AiAccountingAggregateFragment,
|
|
3
|
+
type AiAccountingRecordSetItem,
|
|
4
|
+
aiModelCallCostDetails,
|
|
5
|
+
aiModelCallCostMatchesUsage,
|
|
6
|
+
buildAiAccountingOverviewFromFragments,
|
|
7
|
+
normalizeAiAccountingQuery,
|
|
8
|
+
normalizeAiModelCallAccountingQuery,
|
|
9
|
+
normalizeAiModelCallCostRecord,
|
|
10
|
+
parseAiModelCallCostDetails,
|
|
11
|
+
toAccountingItem,
|
|
12
|
+
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
13
|
+
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
14
|
+
import {
|
|
15
|
+
type AiAccountingOverview,
|
|
16
|
+
type AiCostStorage,
|
|
17
|
+
AiCostStorageError,
|
|
18
|
+
type AiCostSummary,
|
|
19
|
+
type AiModelCallCostRecord,
|
|
20
|
+
type AiModelCallUsageRecord,
|
|
21
|
+
type ListAiModelCallAccountingInput,
|
|
22
|
+
type ListAiModelCallAccountingResult,
|
|
23
|
+
type QueryAiAccountingOverviewInput,
|
|
24
|
+
type SummarizeAiCostExecutionsInput,
|
|
25
|
+
} from "@sixb/core/storage"
|
|
26
|
+
import type { PgStoreClient } from "./transactions"
|
|
27
|
+
import { runPgTransaction } from "./transactions"
|
|
28
|
+
|
|
29
|
+
/** PostgreSQL-backed immutable valuations stored in one append-only table. */
|
|
30
|
+
export class PgAiCostStorage implements AiCostStorage {
|
|
31
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
32
|
+
|
|
33
|
+
async recordModelCallCost(input: AiModelCallCostRecord): Promise<void> {
|
|
34
|
+
const record = normalizeAiModelCallCostRecord(input)
|
|
35
|
+
await runPgTransaction(this.sql, async (tx) => {
|
|
36
|
+
const usage = await findUsage(tx, record.projectId, record.usageRecordId)
|
|
37
|
+
if (!usage) throw missingUsage(record)
|
|
38
|
+
assertCostMatchesUsage(record, usage)
|
|
39
|
+
if (await findCost(tx, record.projectId, record.usageRecordId)) return
|
|
40
|
+
const rated = record.status === "rated" ? record : undefined
|
|
41
|
+
const inserted = await tx<{ usage_record_id: string }[]>`
|
|
42
|
+
INSERT INTO ai_model_call_valuations (
|
|
43
|
+
project_id, usage_record_id, status, provider_id, model_id,
|
|
44
|
+
currency, amount_nanos, reason, details, rated_at
|
|
45
|
+
) VALUES (
|
|
46
|
+
${record.projectId}, ${record.usageRecordId}, ${record.status},
|
|
47
|
+
${record.billingIdentity?.providerId ?? null},
|
|
48
|
+
${record.billingIdentity?.modelId ?? null}, ${rated?.money.currency ?? null},
|
|
49
|
+
${rated?.money.amountNanos ?? null},
|
|
50
|
+
${record.status === "unpriceable" ? record.reason : null},
|
|
51
|
+
${JSON.stringify(aiModelCallCostDetails(record))}::text::jsonb, ${record.ratedAt}
|
|
52
|
+
)
|
|
53
|
+
ON CONFLICT DO NOTHING
|
|
54
|
+
RETURNING usage_record_id
|
|
55
|
+
`
|
|
56
|
+
if (inserted.length === 0 && !(await findCost(tx, record.projectId, record.usageRecordId))) {
|
|
57
|
+
throw new Error("[SixbPg] AI valuation insert conflicted without a visible row.")
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async summarizeExecutions(
|
|
63
|
+
input: SummarizeAiCostExecutionsInput
|
|
64
|
+
): Promise<readonly AiCostSummary[]> {
|
|
65
|
+
assertNonBlank(input.projectId, "cost summary projectId")
|
|
66
|
+
for (const id of input.executionIds) assertNonBlank(id, "cost summary executionId")
|
|
67
|
+
if (input.executionIds.length === 0) return []
|
|
68
|
+
const ids = [...new Set(input.executionIds)]
|
|
69
|
+
const rows = await this.sql<SummaryRow[]>`
|
|
70
|
+
WITH requested(execution_id) AS (
|
|
71
|
+
SELECT DISTINCT * FROM unnest(${this.sql.array(ids)}::text[])
|
|
72
|
+
)
|
|
73
|
+
SELECT usage.execution_id,
|
|
74
|
+
cost.status,
|
|
75
|
+
cost.currency,
|
|
76
|
+
cost.amount_nanos
|
|
77
|
+
FROM requested
|
|
78
|
+
JOIN ai_model_call_usage AS usage
|
|
79
|
+
ON usage.project_id = ${input.projectId} AND usage.execution_id = requested.execution_id
|
|
80
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
81
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
82
|
+
`
|
|
83
|
+
return aggregateSummaryRows(input.executionIds, rows)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async queryProjectOverview(input: QueryAiAccountingOverviewInput): Promise<AiAccountingOverview> {
|
|
87
|
+
const query = normalizeAiAccountingQuery(input)
|
|
88
|
+
const providerId = query.providerId ?? null
|
|
89
|
+
const modelId = query.modelId ?? null
|
|
90
|
+
const rows = await this.sql<AggregateRow[]>`
|
|
91
|
+
WITH filtered AS (
|
|
92
|
+
SELECT usage.provider_id,
|
|
93
|
+
usage.requested_model_id,
|
|
94
|
+
usage.occurred_at,
|
|
95
|
+
usage.input_tokens,
|
|
96
|
+
usage.output_tokens,
|
|
97
|
+
usage.uncached_input_tokens,
|
|
98
|
+
usage.cache_read_input_tokens,
|
|
99
|
+
usage.cache_write_input_tokens,
|
|
100
|
+
usage.text_output_tokens,
|
|
101
|
+
usage.reasoning_output_tokens,
|
|
102
|
+
usage.reporting_status,
|
|
103
|
+
COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
|
|
104
|
+
workflow_node.workflow_id AS attribution_workflow_id,
|
|
105
|
+
cost.status AS cost_status,
|
|
106
|
+
cost.currency AS cost_currency,
|
|
107
|
+
cost.amount_nanos AS cost_amount_nanos
|
|
108
|
+
FROM ai_model_call_usage AS usage
|
|
109
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
110
|
+
ON direct_agent.project_id = usage.project_id
|
|
111
|
+
AND direct_agent.execution_id = usage.execution_id
|
|
112
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
113
|
+
ON workflow_agent.project_id = usage.project_id
|
|
114
|
+
AND workflow_agent.execution_id = usage.execution_id
|
|
115
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
116
|
+
ON workflow_node.project_id = workflow_agent.project_id
|
|
117
|
+
AND workflow_node.id = workflow_agent.node_run_id
|
|
118
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
119
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
120
|
+
WHERE usage.project_id = ${query.projectId}
|
|
121
|
+
AND usage.occurred_at >= ${query.from} AND usage.occurred_at < ${query.to}
|
|
122
|
+
AND (${providerId}::text IS NULL OR usage.provider_id = ${providerId})
|
|
123
|
+
AND (${modelId}::text IS NULL OR usage.requested_model_id = ${modelId})
|
|
124
|
+
), classified AS (
|
|
125
|
+
SELECT filtered.*,
|
|
126
|
+
CASE
|
|
127
|
+
WHEN cost_status = 'rated' THEN 'rated'
|
|
128
|
+
WHEN cost_status = 'unpriceable' THEN 'unpriceable'
|
|
129
|
+
ELSE 'unvalued'
|
|
130
|
+
END AS valuation_status,
|
|
131
|
+
CASE WHEN cost_status = 'rated' THEN cost_currency ELSE NULL END AS amount_currency,
|
|
132
|
+
CASE WHEN cost_status = 'rated' THEN cost_amount_nanos ELSE NULL END AS amount_nanos
|
|
133
|
+
FROM filtered
|
|
134
|
+
), expanded AS (
|
|
135
|
+
SELECT 'totals'::text AS dimension,
|
|
136
|
+
NULL::text AS key_1,
|
|
137
|
+
NULL::text AS key_2,
|
|
138
|
+
NULL::timestamptz AS bucket_start,
|
|
139
|
+
classified.*
|
|
140
|
+
FROM classified
|
|
141
|
+
UNION ALL
|
|
142
|
+
SELECT 'series', NULL, NULL,
|
|
143
|
+
CASE ${input.bucket}::text
|
|
144
|
+
WHEN 'hour' THEN
|
|
145
|
+
date_trunc('hour', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
|
|
146
|
+
WHEN 'day' THEN
|
|
147
|
+
date_trunc('day', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
|
|
148
|
+
ELSE date_trunc('week', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
|
|
149
|
+
END,
|
|
150
|
+
classified.*
|
|
151
|
+
FROM classified
|
|
152
|
+
UNION ALL
|
|
153
|
+
SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
|
|
154
|
+
UNION ALL
|
|
155
|
+
SELECT 'agent', attribution_agent_id, NULL, NULL, classified.*
|
|
156
|
+
FROM classified WHERE attribution_agent_id IS NOT NULL
|
|
157
|
+
UNION ALL
|
|
158
|
+
SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
|
|
159
|
+
FROM classified WHERE attribution_workflow_id IS NOT NULL
|
|
160
|
+
)
|
|
161
|
+
SELECT dimension,
|
|
162
|
+
key_1,
|
|
163
|
+
key_2,
|
|
164
|
+
bucket_start,
|
|
165
|
+
amount_currency,
|
|
166
|
+
COUNT(*)::text AS model_call_count,
|
|
167
|
+
COUNT(*) FILTER (WHERE reporting_status <> 'unavailable')::text
|
|
168
|
+
AS reported_usage_call_count,
|
|
169
|
+
COUNT(input_tokens)::text AS input_tokens_count,
|
|
170
|
+
SUM(input_tokens)::text AS input_tokens_sum,
|
|
171
|
+
COUNT(output_tokens)::text AS output_tokens_count,
|
|
172
|
+
SUM(output_tokens)::text AS output_tokens_sum,
|
|
173
|
+
COUNT(uncached_input_tokens)::text AS uncached_input_tokens_count,
|
|
174
|
+
SUM(uncached_input_tokens)::text AS uncached_input_tokens_sum,
|
|
175
|
+
COUNT(cache_read_input_tokens)::text AS cache_read_input_tokens_count,
|
|
176
|
+
SUM(cache_read_input_tokens)::text AS cache_read_input_tokens_sum,
|
|
177
|
+
COUNT(cache_write_input_tokens)::text AS cache_write_input_tokens_count,
|
|
178
|
+
SUM(cache_write_input_tokens)::text AS cache_write_input_tokens_sum,
|
|
179
|
+
COUNT(text_output_tokens)::text AS text_output_tokens_count,
|
|
180
|
+
SUM(text_output_tokens)::text AS text_output_tokens_sum,
|
|
181
|
+
COUNT(reasoning_output_tokens)::text AS reasoning_output_tokens_count,
|
|
182
|
+
SUM(reasoning_output_tokens)::text AS reasoning_output_tokens_sum,
|
|
183
|
+
COUNT(*) FILTER (WHERE valuation_status = 'rated')::text AS rated_call_count,
|
|
184
|
+
COUNT(*) FILTER (WHERE valuation_status = 'unpriceable')::text
|
|
185
|
+
AS unpriceable_call_count,
|
|
186
|
+
COUNT(*) FILTER (WHERE valuation_status = 'unvalued')::text
|
|
187
|
+
AS unvalued_call_count,
|
|
188
|
+
SUM(amount_nanos)::text AS amount_nanos
|
|
189
|
+
FROM expanded
|
|
190
|
+
GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
|
|
191
|
+
`
|
|
192
|
+
return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async listModelCalls(
|
|
196
|
+
input: ListAiModelCallAccountingInput
|
|
197
|
+
): Promise<ListAiModelCallAccountingResult> {
|
|
198
|
+
const query = normalizeAiModelCallAccountingQuery(input)
|
|
199
|
+
const providerId = query.providerId ?? null
|
|
200
|
+
const modelId = query.modelId ?? null
|
|
201
|
+
const executionId = query.executionId ?? null
|
|
202
|
+
const valuationStatus = query.valuationStatus ?? null
|
|
203
|
+
const [count] = await this.sql<CountRow[]>`
|
|
204
|
+
SELECT COUNT(*)::text AS total
|
|
205
|
+
FROM ai_model_call_usage AS usage
|
|
206
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
207
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
208
|
+
WHERE usage.project_id = ${query.projectId}
|
|
209
|
+
AND usage.occurred_at >= ${query.from} AND usage.occurred_at < ${query.to}
|
|
210
|
+
AND (${providerId}::text IS NULL OR usage.provider_id = ${providerId})
|
|
211
|
+
AND (${modelId}::text IS NULL OR usage.requested_model_id = ${modelId})
|
|
212
|
+
AND (${executionId}::text IS NULL OR usage.execution_id = ${executionId})
|
|
213
|
+
AND (
|
|
214
|
+
${valuationStatus}::text IS NULL OR ${valuationStatus} = CASE
|
|
215
|
+
WHEN cost.status = 'rated' THEN 'rated'
|
|
216
|
+
WHEN cost.status = 'unpriceable' THEN 'unpriceable'
|
|
217
|
+
ELSE 'unvalued'
|
|
218
|
+
END
|
|
219
|
+
)
|
|
220
|
+
`
|
|
221
|
+
const rows = await this.sql<JoinedRow[]>`
|
|
222
|
+
SELECT usage.*,
|
|
223
|
+
CASE
|
|
224
|
+
WHEN direct_agent.id IS NOT NULL THEN 'agent'
|
|
225
|
+
WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
|
|
226
|
+
ELSE NULL
|
|
227
|
+
END AS attribution_kind,
|
|
228
|
+
COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
|
|
229
|
+
direct_agent.id AS attribution_agent_run_id,
|
|
230
|
+
direct_agent.thread_id AS attribution_thread_id,
|
|
231
|
+
workflow_agent.node_run_id AS attribution_node_run_id,
|
|
232
|
+
workflow_node.workflow_id AS attribution_workflow_id,
|
|
233
|
+
workflow_node.workflow_run_id AS attribution_workflow_run_id,
|
|
234
|
+
COALESCE((
|
|
235
|
+
SELECT jsonb_agg(groups.group_id ORDER BY groups.group_id)
|
|
236
|
+
FROM ai_model_call_usage_groups AS groups
|
|
237
|
+
WHERE groups.project_id = usage.project_id AND groups.usage_record_id = usage.id
|
|
238
|
+
), '[]'::jsonb) AS requester_group_ids,
|
|
239
|
+
cost.status AS cost_status,
|
|
240
|
+
cost.provider_id AS cost_provider_id,
|
|
241
|
+
cost.model_id AS cost_model_id,
|
|
242
|
+
cost.currency AS cost_currency,
|
|
243
|
+
cost.amount_nanos AS cost_amount_nanos,
|
|
244
|
+
cost.reason AS cost_reason,
|
|
245
|
+
cost.details AS cost_details,
|
|
246
|
+
cost.rated_at AS cost_rated_at
|
|
247
|
+
FROM ai_model_call_usage AS usage
|
|
248
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
249
|
+
ON direct_agent.project_id = usage.project_id
|
|
250
|
+
AND direct_agent.execution_id = usage.execution_id
|
|
251
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
252
|
+
ON workflow_agent.project_id = usage.project_id
|
|
253
|
+
AND workflow_agent.execution_id = usage.execution_id
|
|
254
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
255
|
+
ON workflow_node.project_id = workflow_agent.project_id
|
|
256
|
+
AND workflow_node.id = workflow_agent.node_run_id
|
|
257
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
258
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
259
|
+
WHERE usage.project_id = ${query.projectId}
|
|
260
|
+
AND usage.occurred_at >= ${query.from} AND usage.occurred_at < ${query.to}
|
|
261
|
+
AND (${providerId}::text IS NULL OR usage.provider_id = ${providerId})
|
|
262
|
+
AND (${modelId}::text IS NULL OR usage.requested_model_id = ${modelId})
|
|
263
|
+
AND (${executionId}::text IS NULL OR usage.execution_id = ${executionId})
|
|
264
|
+
AND (
|
|
265
|
+
${valuationStatus}::text IS NULL OR ${valuationStatus} = CASE
|
|
266
|
+
WHEN cost.status = 'rated' THEN 'rated'
|
|
267
|
+
WHEN cost.status = 'unpriceable' THEN 'unpriceable'
|
|
268
|
+
ELSE 'unvalued'
|
|
269
|
+
END
|
|
270
|
+
)
|
|
271
|
+
ORDER BY usage.occurred_at DESC, usage.id ASC
|
|
272
|
+
LIMIT ${query.limit} OFFSET ${query.offset}
|
|
273
|
+
`
|
|
274
|
+
const items = rows.map((row) => toAccountingItem(accountingItemFromRow(row)))
|
|
275
|
+
if (!count) throw new Error("[SixbPg] AI accounting count query returned no row.")
|
|
276
|
+
const total = safeSqlCount(count.total, "model-call list total")
|
|
277
|
+
return structuredClone({
|
|
278
|
+
items,
|
|
279
|
+
total,
|
|
280
|
+
hasMore: query.offset + items.length < total,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface ValuationRow {
|
|
286
|
+
readonly project_id: string
|
|
287
|
+
readonly usage_record_id: string
|
|
288
|
+
readonly status: "rated" | "unpriceable"
|
|
289
|
+
readonly provider_id: string | null
|
|
290
|
+
readonly model_id: string | null
|
|
291
|
+
readonly currency: string | null
|
|
292
|
+
readonly amount_nanos: string | null
|
|
293
|
+
readonly reason: string | null
|
|
294
|
+
readonly details: unknown
|
|
295
|
+
readonly rated_at: Date | string
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface UsageRow {
|
|
299
|
+
readonly project_id: string
|
|
300
|
+
readonly id: string
|
|
301
|
+
readonly execution_id: string
|
|
302
|
+
readonly attempt: number | string
|
|
303
|
+
readonly call_id: string
|
|
304
|
+
readonly provider_id: string
|
|
305
|
+
readonly requested_model_id: string
|
|
306
|
+
readonly response_model_id: string | null
|
|
307
|
+
readonly response_id: string
|
|
308
|
+
readonly input_tokens: number | string | null
|
|
309
|
+
readonly output_tokens: number | string | null
|
|
310
|
+
readonly total_tokens: number | string | null
|
|
311
|
+
readonly uncached_input_tokens: number | string | null
|
|
312
|
+
readonly cache_read_input_tokens: number | string | null
|
|
313
|
+
readonly cache_write_input_tokens: number | string | null
|
|
314
|
+
readonly text_output_tokens: number | string | null
|
|
315
|
+
readonly reasoning_output_tokens: number | string | null
|
|
316
|
+
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
317
|
+
readonly raw_usage: ReadonlyJsonObject | string | null
|
|
318
|
+
readonly occurred_at: Date | string
|
|
319
|
+
readonly recorded_at: Date | string
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
interface JoinedRow extends UsageRow {
|
|
323
|
+
readonly attribution_kind: "agent" | "workflowAgent" | null
|
|
324
|
+
readonly attribution_agent_id: string | null
|
|
325
|
+
readonly attribution_agent_run_id: string | null
|
|
326
|
+
readonly attribution_thread_id: string | null
|
|
327
|
+
readonly attribution_node_run_id: string | null
|
|
328
|
+
readonly attribution_workflow_id: string | null
|
|
329
|
+
readonly attribution_workflow_run_id: string | null
|
|
330
|
+
readonly requester_group_ids: unknown
|
|
331
|
+
readonly cost_status: ValuationRow["status"] | null
|
|
332
|
+
readonly cost_provider_id: string | null
|
|
333
|
+
readonly cost_model_id: string | null
|
|
334
|
+
readonly cost_currency: string | null
|
|
335
|
+
readonly cost_amount_nanos: string | null
|
|
336
|
+
readonly cost_reason: string | null
|
|
337
|
+
readonly cost_details: unknown
|
|
338
|
+
readonly cost_rated_at: Date | string | null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface AggregateRow {
|
|
342
|
+
readonly dimension: AiAccountingAggregateFragment["dimension"]
|
|
343
|
+
readonly key_1: string | null
|
|
344
|
+
readonly key_2: string | null
|
|
345
|
+
readonly bucket_start: Date | string | null
|
|
346
|
+
readonly amount_currency: string | null
|
|
347
|
+
readonly model_call_count: string
|
|
348
|
+
readonly reported_usage_call_count: string
|
|
349
|
+
readonly input_tokens_count: string
|
|
350
|
+
readonly input_tokens_sum: string | null
|
|
351
|
+
readonly output_tokens_count: string
|
|
352
|
+
readonly output_tokens_sum: string | null
|
|
353
|
+
readonly uncached_input_tokens_count: string
|
|
354
|
+
readonly uncached_input_tokens_sum: string | null
|
|
355
|
+
readonly cache_read_input_tokens_count: string
|
|
356
|
+
readonly cache_read_input_tokens_sum: string | null
|
|
357
|
+
readonly cache_write_input_tokens_count: string
|
|
358
|
+
readonly cache_write_input_tokens_sum: string | null
|
|
359
|
+
readonly text_output_tokens_count: string
|
|
360
|
+
readonly text_output_tokens_sum: string | null
|
|
361
|
+
readonly reasoning_output_tokens_count: string
|
|
362
|
+
readonly reasoning_output_tokens_sum: string | null
|
|
363
|
+
readonly rated_call_count: string
|
|
364
|
+
readonly unpriceable_call_count: string
|
|
365
|
+
readonly unvalued_call_count: string
|
|
366
|
+
readonly amount_nanos: string | null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
interface CountRow {
|
|
370
|
+
readonly total: string
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface SummaryRow {
|
|
374
|
+
readonly execution_id: string
|
|
375
|
+
readonly status: ValuationRow["status"] | null
|
|
376
|
+
readonly currency: string | null
|
|
377
|
+
readonly amount_nanos: string | null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function findCost(
|
|
381
|
+
sql: PgStoreClient,
|
|
382
|
+
projectId: string,
|
|
383
|
+
usageId: string
|
|
384
|
+
): Promise<AiModelCallCostRecord | null> {
|
|
385
|
+
const [row] = await sql<ValuationRow[]>`
|
|
386
|
+
SELECT * FROM ai_model_call_valuations
|
|
387
|
+
WHERE project_id = ${projectId} AND usage_record_id = ${usageId}
|
|
388
|
+
`
|
|
389
|
+
return row ? rowToCost(row) : null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function findUsage(
|
|
393
|
+
sql: PgStoreClient,
|
|
394
|
+
projectId: string,
|
|
395
|
+
id: string
|
|
396
|
+
): Promise<AiModelCallUsageRecord | null> {
|
|
397
|
+
const [row] = await sql<UsageRow[]>`
|
|
398
|
+
SELECT * FROM ai_model_call_usage WHERE project_id = ${projectId} AND id = ${id}
|
|
399
|
+
`
|
|
400
|
+
return row ? usageFromRow(row, []) : null
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragment {
|
|
404
|
+
const dimensionKeys =
|
|
405
|
+
row.dimension === "model"
|
|
406
|
+
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
407
|
+
: row.dimension === "agent"
|
|
408
|
+
? { agentId: required(row.key_1) }
|
|
409
|
+
: row.dimension === "workflow"
|
|
410
|
+
? { workflowId: required(row.key_1) }
|
|
411
|
+
: row.dimension === "series"
|
|
412
|
+
? { start: new Date(required(row.bucket_start)) }
|
|
413
|
+
: {}
|
|
414
|
+
return {
|
|
415
|
+
dimension: row.dimension,
|
|
416
|
+
...dimensionKeys,
|
|
417
|
+
modelCallCount: row.model_call_count,
|
|
418
|
+
usage: {
|
|
419
|
+
reportedCallCount: row.reported_usage_call_count,
|
|
420
|
+
inputTokens: meterFragment(row.input_tokens_count, row.input_tokens_sum),
|
|
421
|
+
outputTokens: meterFragment(row.output_tokens_count, row.output_tokens_sum),
|
|
422
|
+
uncachedInputTokens: meterFragment(
|
|
423
|
+
row.uncached_input_tokens_count,
|
|
424
|
+
row.uncached_input_tokens_sum
|
|
425
|
+
),
|
|
426
|
+
cacheReadInputTokens: meterFragment(
|
|
427
|
+
row.cache_read_input_tokens_count,
|
|
428
|
+
row.cache_read_input_tokens_sum
|
|
429
|
+
),
|
|
430
|
+
cacheWriteInputTokens: meterFragment(
|
|
431
|
+
row.cache_write_input_tokens_count,
|
|
432
|
+
row.cache_write_input_tokens_sum
|
|
433
|
+
),
|
|
434
|
+
textOutputTokens: meterFragment(row.text_output_tokens_count, row.text_output_tokens_sum),
|
|
435
|
+
reasoningOutputTokens: meterFragment(
|
|
436
|
+
row.reasoning_output_tokens_count,
|
|
437
|
+
row.reasoning_output_tokens_sum
|
|
438
|
+
),
|
|
439
|
+
},
|
|
440
|
+
costs: {
|
|
441
|
+
...(row.amount_currency === null || row.amount_nanos === null
|
|
442
|
+
? {}
|
|
443
|
+
: {
|
|
444
|
+
amount: {
|
|
445
|
+
currency: row.amount_currency,
|
|
446
|
+
amountNanos: row.amount_nanos,
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
449
|
+
ratedCallCount: row.rated_call_count,
|
|
450
|
+
unpriceableCallCount: row.unpriceable_call_count,
|
|
451
|
+
unvaluedCallCount: row.unvalued_call_count,
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function meterFragment(presentCallCount: string, total: string | null) {
|
|
457
|
+
return { presentCallCount, total }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function accountingItemFromRow(row: JoinedRow): AiAccountingRecordSetItem {
|
|
461
|
+
const usage = usageFromRow(row, parseStringArray(row.requester_group_ids))
|
|
462
|
+
const cost = row.cost_status === null ? undefined : costFromJoinedRow(row)
|
|
463
|
+
const attribution =
|
|
464
|
+
row.attribution_kind === null
|
|
465
|
+
? undefined
|
|
466
|
+
: row.attribution_kind === "agent"
|
|
467
|
+
? {
|
|
468
|
+
kind: "agent" as const,
|
|
469
|
+
agentId: required(row.attribution_agent_id),
|
|
470
|
+
agentRunId: required(row.attribution_agent_run_id),
|
|
471
|
+
threadId: required(row.attribution_thread_id),
|
|
472
|
+
}
|
|
473
|
+
: {
|
|
474
|
+
kind: "workflowAgent" as const,
|
|
475
|
+
agentId: required(row.attribution_agent_id),
|
|
476
|
+
nodeRunId: required(row.attribution_node_run_id),
|
|
477
|
+
workflowId: required(row.attribution_workflow_id),
|
|
478
|
+
workflowRunId: required(row.attribution_workflow_run_id),
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
usage,
|
|
482
|
+
...(attribution ? { attribution } : {}),
|
|
483
|
+
...(cost ? { cost } : {}),
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiModelCallUsageRecord {
|
|
488
|
+
const number = (value: number | string | null) => (value === null ? undefined : Number(value))
|
|
489
|
+
return {
|
|
490
|
+
id: row.id,
|
|
491
|
+
projectId: row.project_id,
|
|
492
|
+
executionId: row.execution_id,
|
|
493
|
+
attempt: Number(row.attempt),
|
|
494
|
+
callId: row.call_id,
|
|
495
|
+
requesterGroupIds,
|
|
496
|
+
providerId: row.provider_id,
|
|
497
|
+
requestedModelId: row.requested_model_id,
|
|
498
|
+
...(row.response_model_id === null ? {} : { responseModelId: row.response_model_id }),
|
|
499
|
+
responseId: row.response_id,
|
|
500
|
+
usage: {
|
|
501
|
+
...(number(row.input_tokens) === undefined ? {} : { inputTokens: number(row.input_tokens) }),
|
|
502
|
+
...(number(row.output_tokens) === undefined
|
|
503
|
+
? {}
|
|
504
|
+
: { outputTokens: number(row.output_tokens) }),
|
|
505
|
+
...(number(row.total_tokens) === undefined ? {} : { totalTokens: number(row.total_tokens) }),
|
|
506
|
+
...(number(row.uncached_input_tokens) === undefined
|
|
507
|
+
? {}
|
|
508
|
+
: { uncachedInputTokens: number(row.uncached_input_tokens) }),
|
|
509
|
+
...(number(row.cache_read_input_tokens) === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { cacheReadInputTokens: number(row.cache_read_input_tokens) }),
|
|
512
|
+
...(number(row.cache_write_input_tokens) === undefined
|
|
513
|
+
? {}
|
|
514
|
+
: { cacheWriteInputTokens: number(row.cache_write_input_tokens) }),
|
|
515
|
+
...(number(row.text_output_tokens) === undefined
|
|
516
|
+
? {}
|
|
517
|
+
: { textOutputTokens: number(row.text_output_tokens) }),
|
|
518
|
+
...(number(row.reasoning_output_tokens) === undefined
|
|
519
|
+
? {}
|
|
520
|
+
: { reasoningOutputTokens: number(row.reasoning_output_tokens) }),
|
|
521
|
+
reportingStatus: row.reporting_status,
|
|
522
|
+
},
|
|
523
|
+
...(row.raw_usage === null ? {} : { rawUsage: parseJsonObject(row.raw_usage) }),
|
|
524
|
+
occurredAt: new Date(row.occurred_at),
|
|
525
|
+
recordedAt: new Date(row.recorded_at),
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function rowToCost(row: ValuationRow): AiModelCallCostRecord {
|
|
530
|
+
return costRecord({
|
|
531
|
+
projectId: row.project_id,
|
|
532
|
+
usageId: row.usage_record_id,
|
|
533
|
+
status: row.status,
|
|
534
|
+
providerId: row.provider_id,
|
|
535
|
+
modelId: row.model_id,
|
|
536
|
+
currency: row.currency,
|
|
537
|
+
amountNanos: row.amount_nanos,
|
|
538
|
+
reason: row.reason,
|
|
539
|
+
details: row.details,
|
|
540
|
+
ratedAt: row.rated_at,
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function costFromJoinedRow(row: JoinedRow): AiModelCallCostRecord {
|
|
545
|
+
return costRecord({
|
|
546
|
+
projectId: row.project_id,
|
|
547
|
+
usageId: row.id,
|
|
548
|
+
status: required(row.cost_status),
|
|
549
|
+
providerId: row.cost_provider_id,
|
|
550
|
+
modelId: row.cost_model_id,
|
|
551
|
+
currency: row.cost_currency,
|
|
552
|
+
amountNanos: row.cost_amount_nanos,
|
|
553
|
+
reason: row.cost_reason,
|
|
554
|
+
details: row.cost_details,
|
|
555
|
+
ratedAt: required(row.cost_rated_at),
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function costRecord(input: {
|
|
560
|
+
projectId: string
|
|
561
|
+
usageId: string
|
|
562
|
+
status: ValuationRow["status"]
|
|
563
|
+
providerId: string | null
|
|
564
|
+
modelId: string | null
|
|
565
|
+
currency: string | null
|
|
566
|
+
amountNanos: string | null
|
|
567
|
+
reason: string | null
|
|
568
|
+
details: unknown
|
|
569
|
+
ratedAt: Date | string
|
|
570
|
+
}): AiModelCallCostRecord {
|
|
571
|
+
const details = parseAiModelCallCostDetails(input.details)
|
|
572
|
+
const base = {
|
|
573
|
+
projectId: input.projectId,
|
|
574
|
+
usageRecordId: input.usageId,
|
|
575
|
+
pricingContext: details.pricingContext,
|
|
576
|
+
priceSource: {
|
|
577
|
+
...details.priceSource,
|
|
578
|
+
observedAt: new Date(details.priceSource.observedAt),
|
|
579
|
+
},
|
|
580
|
+
ratedAt: new Date(input.ratedAt),
|
|
581
|
+
}
|
|
582
|
+
if (input.status === "rated") {
|
|
583
|
+
return normalizeAiModelCallCostRecord({
|
|
584
|
+
...base,
|
|
585
|
+
status: "rated",
|
|
586
|
+
billingIdentity: { providerId: required(input.providerId), modelId: required(input.modelId) },
|
|
587
|
+
money: { currency: required(input.currency), amountNanos: required(input.amountNanos) },
|
|
588
|
+
components: required(details.components),
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
return normalizeAiModelCallCostRecord({
|
|
592
|
+
...base,
|
|
593
|
+
status: "unpriceable",
|
|
594
|
+
...(input.providerId && input.modelId
|
|
595
|
+
? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } }
|
|
596
|
+
: {}),
|
|
597
|
+
reason: required(input.reason) as Extract<
|
|
598
|
+
AiModelCallCostRecord,
|
|
599
|
+
{ status: "unpriceable" }
|
|
600
|
+
>["reason"],
|
|
601
|
+
...(details.missingMeters ? { missingMeters: details.missingMeters } : {}),
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function aggregateSummaryRows(
|
|
606
|
+
ids: readonly string[],
|
|
607
|
+
rows: readonly SummaryRow[]
|
|
608
|
+
): AiCostSummary[] {
|
|
609
|
+
const map = new Map(ids.map((id) => [id, accumulator()]))
|
|
610
|
+
for (const row of rows) {
|
|
611
|
+
const value = map.get(row.execution_id)
|
|
612
|
+
if (!value) continue
|
|
613
|
+
if (row.status === null) value.unvaluedCallCount += 1
|
|
614
|
+
else if (row.status === "unpriceable") value.unpriceableCallCount += 1
|
|
615
|
+
else {
|
|
616
|
+
value.ratedCallCount += 1
|
|
617
|
+
add(value.amounts, required(row.currency), required(row.amount_nanos))
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return ids.map((id) => finish(map.get(id)!))
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
interface Accumulator {
|
|
624
|
+
amounts: Map<string, bigint>
|
|
625
|
+
ratedCallCount: number
|
|
626
|
+
unpriceableCallCount: number
|
|
627
|
+
unvaluedCallCount: number
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function accumulator(): Accumulator {
|
|
631
|
+
return {
|
|
632
|
+
amounts: new Map(),
|
|
633
|
+
ratedCallCount: 0,
|
|
634
|
+
unpriceableCallCount: 0,
|
|
635
|
+
unvaluedCallCount: 0,
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function finish(value: Accumulator): AiCostSummary {
|
|
640
|
+
return {
|
|
641
|
+
amounts: [...value.amounts]
|
|
642
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
643
|
+
.map(([currency, amount]) => ({ currency, amountNanos: amount.toString() })),
|
|
644
|
+
ratedCallCount: value.ratedCallCount,
|
|
645
|
+
unpriceableCallCount: value.unpriceableCallCount,
|
|
646
|
+
unvaluedCallCount: value.unvaluedCallCount,
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function add(amounts: Map<string, bigint>, currency: string, amount: string): void {
|
|
651
|
+
amounts.set(currency, (amounts.get(currency) ?? 0n) + BigInt(amount))
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function parseStringArray(value: unknown): string[] {
|
|
655
|
+
const parsed: unknown = typeof value === "string" ? JSON.parse(value) : value
|
|
656
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
657
|
+
throw new Error("[SixbPg] AI requester group ids are invalid.")
|
|
658
|
+
}
|
|
659
|
+
return parsed
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function parseJsonObject(value: unknown): ReadonlyJsonObject {
|
|
663
|
+
return (typeof value === "string" ? JSON.parse(value) : value) as ReadonlyJsonObject
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function assertCostMatchesUsage(
|
|
667
|
+
record: AiModelCallCostRecord,
|
|
668
|
+
usage: AiModelCallUsageRecord
|
|
669
|
+
): void {
|
|
670
|
+
if (!aiModelCallCostMatchesUsage(record, usage)) {
|
|
671
|
+
throw new AiCostStorageError(
|
|
672
|
+
"cost_mismatch",
|
|
673
|
+
`[SixbPg] AI valuation for usage '${record.usageRecordId}' does not match its usage record.`
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function missingUsage(record: { projectId: string; usageRecordId: string }) {
|
|
679
|
+
return new AiCostStorageError(
|
|
680
|
+
"missing_usage",
|
|
681
|
+
`[SixbPg] AI usage '${record.usageRecordId}' does not exist.`
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function assertNonBlank(value: string, field: string): void {
|
|
686
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
687
|
+
throw new TypeError(`[Sixb] AI ${field} must be nonblank.`)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function safeSqlCount(value: string, field: string): number {
|
|
692
|
+
if (!/^(0|[1-9]\d*)$/.test(value)) {
|
|
693
|
+
throw new Error(`[SixbPg] AI accounting ${field} is invalid.`)
|
|
694
|
+
}
|
|
695
|
+
const count = Number(value)
|
|
696
|
+
if (!Number.isSafeInteger(count)) {
|
|
697
|
+
throw new TypeError(`[Sixb] AI accounting ${field} exceeds the safe integer range.`)
|
|
698
|
+
}
|
|
699
|
+
return count
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function required<T>(value: T | null | undefined): T {
|
|
703
|
+
if (value === null || value === undefined) {
|
|
704
|
+
throw new Error("[SixbPg] AI valuation column is unexpectedly null.")
|
|
705
|
+
}
|
|
706
|
+
return value
|
|
707
|
+
}
|