@sqdr2/dsh-fusion 0.0.0-stage → 0.3.2
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/INSTALL.md +89 -0
- package/INSTALL.zh.md +86 -0
- package/LICENSE +201 -0
- package/README.en.md +193 -0
- package/README.md +182 -2
- package/RELEASING.md +123 -0
- package/RELEASING.zh.md +115 -0
- package/contracts/fusion.ts +279 -0
- package/contracts/storage.sql +86 -0
- package/cordis.patch.yml +3 -0
- package/docs/COMPATIBILITY.md +162 -0
- package/docs/EVIDENCE.md +141 -0
- package/dshx.yml +39 -0
- package/lib/client.js +1920 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-fusion.js +12664 -0
- package/lib/types/activity.d.ts +20 -0
- package/lib/types/approval/logical.d.ts +10 -0
- package/lib/types/benchmark/naive.d.ts +37 -0
- package/lib/types/benchmark/native-run.d.ts +68 -0
- package/lib/types/benchmark/output-limits.d.ts +19 -0
- package/lib/types/bilingual.d.ts +2 -0
- package/lib/types/client/ActivityRows.d.ts +8 -0
- package/lib/types/client/FusionCache.d.ts +4 -0
- package/lib/types/client/FusionHint.d.ts +9 -0
- package/lib/types/client/FusionHistory.d.ts +5 -0
- package/lib/types/client/FusionPanelIcon.d.ts +13 -0
- package/lib/types/client/FusionRecovery.d.ts +5 -0
- package/lib/types/client/FusionSettings.d.ts +9 -0
- package/lib/types/client/FusionStatus.d.ts +5 -0
- package/lib/types/client/FusionTool.d.ts +4 -0
- package/lib/types/client/activity-model.d.ts +14 -0
- package/lib/types/client/i18n.d.ts +15 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/settings-events.d.ts +2 -0
- package/lib/types/client/status-polling.d.ts +9 -0
- package/lib/types/context/guard.d.ts +21 -0
- package/lib/types/context/handoff.d.ts +17 -0
- package/lib/types/contracts.d.ts +449 -0
- package/lib/types/digest.d.ts +5 -0
- package/lib/types/dsh-fusion.d.ts +13 -0
- package/lib/types/errors.d.ts +28 -0
- package/lib/types/evidence/gate.d.ts +23 -0
- package/lib/types/evidence/receipts.d.ts +65 -0
- package/lib/types/execution/write-lease.d.ts +42 -0
- package/lib/types/host/bindings.d.ts +30 -0
- package/lib/types/host/cache-defaults.d.ts +48 -0
- package/lib/types/host/cache-policy.d.ts +155 -0
- package/lib/types/host/change-evidence.d.ts +38 -0
- package/lib/types/host/compaction-routing.d.ts +15 -0
- package/lib/types/host/coordinator.d.ts +169 -0
- package/lib/types/host/evidence-page.d.ts +13 -0
- package/lib/types/host/history.d.ts +88 -0
- package/lib/types/host/http.d.ts +4 -0
- package/lib/types/host/model-output.d.ts +13 -0
- package/lib/types/host/native-activity.d.ts +26 -0
- package/lib/types/host/native-approvals.d.ts +29 -0
- package/lib/types/host/native-auxiliary.d.ts +17 -0
- package/lib/types/host/native-budget.d.ts +33 -0
- package/lib/types/host/native-checks.d.ts +104 -0
- package/lib/types/host/native-compaction.d.ts +8 -0
- package/lib/types/host/native-context.d.ts +36 -0
- package/lib/types/host/native-effects.d.ts +75 -0
- package/lib/types/host/native-evidence.d.ts +4 -0
- package/lib/types/host/native-exploration.d.ts +15 -0
- package/lib/types/host/native-keepalive.d.ts +82 -0
- package/lib/types/host/native-model-control.d.ts +99 -0
- package/lib/types/host/native-on-demand-context.d.ts +39 -0
- package/lib/types/host/native-request.d.ts +5 -0
- package/lib/types/host/native-review.d.ts +11 -0
- package/lib/types/host/native-role-sandbox.d.ts +49 -0
- package/lib/types/host/native-scopes.d.ts +49 -0
- package/lib/types/host/native-selection.d.ts +29 -0
- package/lib/types/host/native-task-context.d.ts +39 -0
- package/lib/types/host/native-usage.d.ts +46 -0
- package/lib/types/host/native-worker-brief.d.ts +74 -0
- package/lib/types/host/native-worker-notices.d.ts +15 -0
- package/lib/types/host/native-worker.d.ts +44 -0
- package/lib/types/host/native-workflow.d.ts +41 -0
- package/lib/types/host/route-transport.d.ts +22 -0
- package/lib/types/host/settings.d.ts +39 -0
- package/lib/types/host/shell.d.ts +11 -0
- package/lib/types/host/status.d.ts +7 -0
- package/lib/types/host/subagent-seam.d.ts +10 -0
- package/lib/types/host/tool-table.d.ts +35 -0
- package/lib/types/host/worker-terminal.d.ts +68 -0
- package/lib/types/host/workspace-guard.d.ts +13 -0
- package/lib/types/host/workspace.d.ts +52 -0
- package/lib/types/identity.d.ts +64 -0
- package/lib/types/index.d.ts +34 -0
- package/lib/types/message-source.d.ts +29 -0
- package/lib/types/profile/compactor.d.ts +5 -0
- package/lib/types/profile/keepalive.d.ts +3 -0
- package/lib/types/profile/resolve.d.ts +17 -0
- package/lib/types/prompts.d.ts +8 -0
- package/lib/types/recovery/reconcile.d.ts +28 -0
- package/lib/types/review/dispatch.d.ts +20 -0
- package/lib/types/review/protocol.d.ts +51 -0
- package/lib/types/review/ticket.d.ts +32 -0
- package/lib/types/status.d.ts +88 -0
- package/lib/types/store-migration.d.ts +29 -0
- package/lib/types/task/deletion.d.ts +64 -0
- package/lib/types/task/reducer.d.ts +6 -0
- package/lib/types/task/sqlite-store.d.ts +71 -0
- package/lib/types/task/state.d.ts +51 -0
- package/lib/types/task/status-index.d.ts +28 -0
- package/lib/types/task/store.d.ts +48 -0
- package/lib/types/usage/budget.d.ts +27 -0
- package/lib/types/usage/ledger.d.ts +66 -0
- package/lib/types/usage/normalize.d.ts +50 -0
- package/lib/types/usage/pricing.d.ts +18 -0
- package/package.json +178 -4
- package/profiles/fusion-auto.example.json +45 -0
- package/prompts/compact.md +7 -0
- package/prompts/lead.md +42 -0
- package/prompts/model-lead.md +13 -0
- package/prompts/model-worker.md +5 -0
- package/prompts/worker.md +21 -0
- package/src/activity.ts +23 -0
- package/src/approval/logical.ts +44 -0
- package/src/benchmark/naive.ts +216 -0
- package/src/benchmark/native-run.ts +215 -0
- package/src/benchmark/output-limits.ts +33 -0
- package/src/bilingual.ts +2 -0
- package/src/client/ActivityRows.tsx +59 -0
- package/src/client/FusionCache.tsx +130 -0
- package/src/client/FusionHint.tsx +39 -0
- package/src/client/FusionHistory.tsx +247 -0
- package/src/client/FusionPanelIcon.tsx +39 -0
- package/src/client/FusionRecovery.tsx +69 -0
- package/src/client/FusionSettings.tsx +205 -0
- package/src/client/FusionStatus.tsx +103 -0
- package/src/client/FusionTool.tsx +66 -0
- package/src/client/activity-model.ts +31 -0
- package/src/client/i18n.ts +30 -0
- package/src/client/index.ts +50 -0
- package/src/client/settings-events.ts +3 -0
- package/src/client/status-polling.ts +88 -0
- package/src/context/guard.ts +81 -0
- package/src/context/handoff.ts +28 -0
- package/src/contracts.ts +449 -0
- package/src/digest.ts +28 -0
- package/src/dsh-fusion.ts +321 -0
- package/src/errors.ts +34 -0
- package/src/evidence/gate.ts +290 -0
- package/src/evidence/receipts.ts +175 -0
- package/src/execution/write-lease.ts +187 -0
- package/src/host/bindings.ts +91 -0
- package/src/host/cache-defaults.ts +103 -0
- package/src/host/cache-policy.ts +168 -0
- package/src/host/change-evidence.ts +156 -0
- package/src/host/compaction-routing.ts +46 -0
- package/src/host/coordinator.ts +2160 -0
- package/src/host/evidence-page.ts +25 -0
- package/src/host/history.ts +114 -0
- package/src/host/http.ts +30 -0
- package/src/host/model-output.ts +22 -0
- package/src/host/native-activity.ts +199 -0
- package/src/host/native-approvals.ts +196 -0
- package/src/host/native-auxiliary.ts +19 -0
- package/src/host/native-budget.ts +112 -0
- package/src/host/native-checks.ts +474 -0
- package/src/host/native-compaction.ts +34 -0
- package/src/host/native-context.ts +204 -0
- package/src/host/native-effects.ts +260 -0
- package/src/host/native-evidence.ts +31 -0
- package/src/host/native-exploration.ts +114 -0
- package/src/host/native-keepalive.ts +292 -0
- package/src/host/native-model-control.ts +298 -0
- package/src/host/native-on-demand-context.ts +74 -0
- package/src/host/native-request.ts +12 -0
- package/src/host/native-review.ts +51 -0
- package/src/host/native-role-sandbox.ts +171 -0
- package/src/host/native-scopes.ts +203 -0
- package/src/host/native-selection.ts +110 -0
- package/src/host/native-task-context.ts +264 -0
- package/src/host/native-usage.ts +121 -0
- package/src/host/native-worker-brief.ts +122 -0
- package/src/host/native-worker-notices.ts +72 -0
- package/src/host/native-worker.ts +215 -0
- package/src/host/native-workflow.ts +103 -0
- package/src/host/route-transport.ts +54 -0
- package/src/host/settings.ts +93 -0
- package/src/host/shell.ts +13 -0
- package/src/host/status.ts +112 -0
- package/src/host/subagent-seam.ts +20 -0
- package/src/host/tool-table.ts +56 -0
- package/src/host/worker-terminal.ts +91 -0
- package/src/host/workspace-guard.ts +17 -0
- package/src/host/workspace.ts +184 -0
- package/src/identity.ts +76 -0
- package/src/index.ts +50 -0
- package/src/message-source.ts +31 -0
- package/src/profile/compactor.ts +26 -0
- package/src/profile/keepalive.ts +13 -0
- package/src/profile/resolve.ts +131 -0
- package/src/prompts.ts +32 -0
- package/src/recovery/reconcile.ts +38 -0
- package/src/review/dispatch.ts +48 -0
- package/src/review/protocol.ts +212 -0
- package/src/review/ticket.ts +166 -0
- package/src/status.ts +55 -0
- package/src/store-migration.ts +65 -0
- package/src/task/deletion.ts +105 -0
- package/src/task/reducer.ts +679 -0
- package/src/task/sqlite-store.ts +386 -0
- package/src/task/state.ts +72 -0
- package/src/task/status-index.ts +198 -0
- package/src/task/store.ts +287 -0
- package/src/usage/budget.ts +88 -0
- package/src/usage/ledger.ts +218 -0
- package/src/usage/normalize.ts +308 -0
- package/src/usage/pricing.ts +95 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { NormalizedUsage, ReasoningBilling, UsageApplicability, UsdDecimal } from '../contracts.js'
|
|
2
|
+
import { UsdDecimal as usd } from '../contracts.js'
|
|
3
|
+
import { FusionError, USAGE_LEDGER_REQUIRED } from '../errors.js'
|
|
4
|
+
import {
|
|
5
|
+
known,
|
|
6
|
+
na,
|
|
7
|
+
splitInput,
|
|
8
|
+
unknown,
|
|
9
|
+
type CanonicalBill,
|
|
10
|
+
type Count,
|
|
11
|
+
type UsageObservation,
|
|
12
|
+
type UsageRequestKey,
|
|
13
|
+
} from './ledger.js'
|
|
14
|
+
|
|
15
|
+
export interface RawUsage {
|
|
16
|
+
inputTokens?: number | null
|
|
17
|
+
outputTokens?: number | null
|
|
18
|
+
cacheReadTokens?: number | null
|
|
19
|
+
cacheWriteTokens?: number | null
|
|
20
|
+
cacheWriteByTtl?: Readonly<Record<string, number | null>>
|
|
21
|
+
reasoningTokens?: number | null
|
|
22
|
+
/** When true, inputTokens already includes cacheReadTokens. */
|
|
23
|
+
inputIncludesCacheRead?: boolean
|
|
24
|
+
/** When true, outputTokens already includes reasoningTokens. */
|
|
25
|
+
outputIncludesReasoning?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PriceCard {
|
|
29
|
+
digest: string
|
|
30
|
+
uncachedInputPerMillion?: string | null
|
|
31
|
+
cacheReadPerMillion?: string | null
|
|
32
|
+
cacheWritePerMillion?: Readonly<Record<string, string | null>>
|
|
33
|
+
outputPerMillion?: string | null
|
|
34
|
+
reasoningPerMillion?: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type UsageObservationKind = 'partial' | 'final'
|
|
38
|
+
|
|
39
|
+
export interface UsageObservationMeta {
|
|
40
|
+
requestId?: string
|
|
41
|
+
source?: UsageObservationKind
|
|
42
|
+
sequence?: number
|
|
43
|
+
cumulative?: boolean
|
|
44
|
+
observationId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type UsagePriceStatus = 'complete' | 'incomplete' | 'unknown'
|
|
48
|
+
|
|
49
|
+
export interface UsagePrice {
|
|
50
|
+
status: UsagePriceStatus
|
|
51
|
+
/** Complete total only. Never a partial stand-in for ranking. */
|
|
52
|
+
totalUsd: UsdDecimal | null
|
|
53
|
+
/** Lower bound of priced known buckets. Not a substitute total. */
|
|
54
|
+
observedUsd: UsdDecimal | null
|
|
55
|
+
reasons: readonly string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function finite(value: number | null | undefined): number | null {
|
|
59
|
+
if (value === null || value === undefined) return null
|
|
60
|
+
if (!Number.isFinite(value) || value < 0) throw new TypeError(`usage must be a finite nonnegative number, got ${value}`)
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function applicability(value: number | null | undefined, present: boolean): UsageApplicability {
|
|
65
|
+
if (!present) return 'not_applicable'
|
|
66
|
+
if (value === null || value === undefined) return 'unknown'
|
|
67
|
+
return 'known'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toCount(value: number | null, status: UsageApplicability): Count {
|
|
71
|
+
if (status === 'not_applicable') return na()
|
|
72
|
+
if (status === 'unknown' || value === null) return unknown()
|
|
73
|
+
return known(value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fromCount(value: Count): number | null {
|
|
77
|
+
return value.state === 'known' ? value.tokens : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function reasoningFromRaw(raw: RawUsage): ReasoningBilling {
|
|
81
|
+
const present = Object.prototype.hasOwnProperty.call(raw, 'reasoningTokens')
|
|
82
|
+
const tokens = finite(raw.reasoningTokens)
|
|
83
|
+
if (raw.outputIncludesReasoning === true) return { kind: 'included', tokens }
|
|
84
|
+
if (raw.outputIncludesReasoning === false) return { kind: 'separate', tokens }
|
|
85
|
+
if (!present) return { kind: 'not_applicable', tokens: null }
|
|
86
|
+
return { kind: 'unknown', tokens }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeUsage(raw: RawUsage): NormalizedUsage {
|
|
90
|
+
const cacheReadPresent = Object.prototype.hasOwnProperty.call(raw, 'cacheReadTokens')
|
|
91
|
+
const cacheRead = finite(raw.cacheReadTokens)
|
|
92
|
+
const input = finite(raw.inputTokens)
|
|
93
|
+
const includesCache = raw.inputIncludesCacheRead ?? null
|
|
94
|
+
const cacheReadStatus = applicability(raw.cacheReadTokens, cacheReadPresent)
|
|
95
|
+
const split = splitInput(toCount(input, input === null ? 'unknown' : 'known'), toCount(cacheRead, cacheReadStatus), includesCache)
|
|
96
|
+
const cacheWritePresent = Object.prototype.hasOwnProperty.call(raw, 'cacheWriteTokens')
|
|
97
|
+
|| Object.prototype.hasOwnProperty.call(raw, 'cacheWriteByTtl')
|
|
98
|
+
let cacheWrite: Record<string, number | null> = {}
|
|
99
|
+
let cacheWriteStatus: Record<string, UsageApplicability> = {}
|
|
100
|
+
if (raw.cacheWriteByTtl) {
|
|
101
|
+
cacheWrite = Object.fromEntries(Object.entries(raw.cacheWriteByTtl).map(([ttl, tokens]) => [ttl, finite(tokens)]))
|
|
102
|
+
cacheWriteStatus = Object.fromEntries(
|
|
103
|
+
Object.entries(raw.cacheWriteByTtl).map(([ttl, tokens]) => [ttl, applicability(tokens, true)]),
|
|
104
|
+
)
|
|
105
|
+
} else if (cacheWritePresent) {
|
|
106
|
+
cacheWrite = { default: finite(raw.cacheWriteTokens) }
|
|
107
|
+
cacheWriteStatus = { default: applicability(raw.cacheWriteTokens, true) }
|
|
108
|
+
}
|
|
109
|
+
const output = finite(raw.outputTokens)
|
|
110
|
+
const reasoningBilling = reasoningFromRaw(raw)
|
|
111
|
+
return {
|
|
112
|
+
uncachedInput: fromCount(split),
|
|
113
|
+
cacheRead,
|
|
114
|
+
cacheReadStatus,
|
|
115
|
+
cacheWrite,
|
|
116
|
+
cacheWriteStatus,
|
|
117
|
+
output,
|
|
118
|
+
reasoningOutputSubset: reasoningBilling.kind === 'included' ? reasoningBilling.tokens : null,
|
|
119
|
+
reasoningSeparate: reasoningBilling.kind === 'separate' ? reasoningBilling.tokens : null,
|
|
120
|
+
reasoningUnspecified: reasoningBilling.kind === 'unknown' ? reasoningBilling.tokens : null,
|
|
121
|
+
reasoningBilling,
|
|
122
|
+
inputIncludesCacheRead: includesCache,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cacheWritesFromUsage(usage: NormalizedUsage): CanonicalBill['cacheWrite'] {
|
|
127
|
+
const keys = Object.keys(usage.cacheWrite)
|
|
128
|
+
if (!keys.length) return { kind: 'not_applicable' }
|
|
129
|
+
if (keys.some(key => usage.cacheWriteStatus[key] === 'unknown' || usage.cacheWrite[key] === null)) {
|
|
130
|
+
if (keys.length === 1 && keys[0] === 'default') {
|
|
131
|
+
return usage.cacheWriteStatus.default === 'unknown' || usage.cacheWrite.default === null
|
|
132
|
+
? { kind: 'unknown' }
|
|
133
|
+
: { kind: 'aggregate', rateKey: 'default', tokens: toCount(usage.cacheWrite.default ?? null, usage.cacheWriteStatus.default ?? 'known') }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (keys.length === 1 && keys[0] === 'default') {
|
|
137
|
+
return {
|
|
138
|
+
kind: 'aggregate',
|
|
139
|
+
rateKey: 'default',
|
|
140
|
+
tokens: toCount(usage.cacheWrite.default ?? null, usage.cacheWriteStatus.default ?? (usage.cacheWrite.default === null ? 'unknown' : 'known')),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
kind: 'details',
|
|
145
|
+
buckets: Object.fromEntries(keys.map(key => [key, {
|
|
146
|
+
rateKey: key,
|
|
147
|
+
tokens: toCount(usage.cacheWrite[key] ?? null, usage.cacheWriteStatus[key] ?? (usage.cacheWrite[key] === null ? 'unknown' : 'known')),
|
|
148
|
+
}])),
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function billFromUsage(usage: NormalizedUsage): CanonicalBill {
|
|
153
|
+
const billing = usage.reasoningBilling ?? (
|
|
154
|
+
usage.reasoningOutputSubset !== null
|
|
155
|
+
? { kind: 'included' as const, tokens: usage.reasoningOutputSubset }
|
|
156
|
+
: usage.reasoningSeparate !== null
|
|
157
|
+
? { kind: 'separate' as const, tokens: usage.reasoningSeparate }
|
|
158
|
+
: usage.reasoningUnspecified !== null
|
|
159
|
+
? { kind: 'unknown' as const, tokens: usage.reasoningUnspecified }
|
|
160
|
+
: { kind: 'not_applicable' as const, tokens: null }
|
|
161
|
+
)
|
|
162
|
+
return {
|
|
163
|
+
uncachedInput: toCount(usage.uncachedInput, usage.uncachedInput === null ? 'unknown' : 'known'),
|
|
164
|
+
cacheRead: toCount(usage.cacheRead, usage.cacheReadStatus),
|
|
165
|
+
output: toCount(usage.output, usage.output === null ? 'unknown' : 'known'),
|
|
166
|
+
cacheWrite: cacheWritesFromUsage(usage),
|
|
167
|
+
reasoning: {
|
|
168
|
+
kind: billing.kind,
|
|
169
|
+
tokens: billing.kind === 'not_applicable' ? na() : toCount(billing.tokens, billing.tokens === null ? 'unknown' : 'known'),
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function observationMode(meta?: UsageObservationMeta): UsageObservation['mode'] {
|
|
175
|
+
if (meta?.source === 'final') return 'final'
|
|
176
|
+
if (meta?.cumulative === false) return 'delta'
|
|
177
|
+
return 'snapshot'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function asObservation(usage: NormalizedUsage, meta: UsageObservationMeta | undefined, key: UsageRequestKey, side: 'first' | 'second'): UsageObservation {
|
|
181
|
+
const bill = billFromUsage(usage)
|
|
182
|
+
const sequence = meta?.sequence ?? (side === 'first' ? 0 : 1)
|
|
183
|
+
return {
|
|
184
|
+
id: meta?.observationId ?? `anon:${side}:${sequence}:${JSON.stringify(bill)}`,
|
|
185
|
+
key,
|
|
186
|
+
sequence,
|
|
187
|
+
mode: observationMode(meta),
|
|
188
|
+
bill,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function observationFromNormalized(
|
|
193
|
+
usage: NormalizedUsage,
|
|
194
|
+
key: UsageRequestKey,
|
|
195
|
+
meta: UsageObservationMeta & { id?: string },
|
|
196
|
+
): UsageObservation {
|
|
197
|
+
return asObservation(usage, { ...meta, observationId: meta.id ?? meta.observationId }, key, 'first')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Retired. Persist UsageLedgerV2 and quoteLedger(). A NormalizedUsage projection
|
|
202
|
+
* is not enough history to replay a request bill.
|
|
203
|
+
*/
|
|
204
|
+
export function mergeUsageObservations(
|
|
205
|
+
..._args: unknown[]
|
|
206
|
+
): never {
|
|
207
|
+
throw new FusionError(
|
|
208
|
+
USAGE_LEDGER_REQUIRED,
|
|
209
|
+
'persist UsageLedgerV2 with ingestDurable/restoreLedger; mergeUsageObservations cannot recover request history',
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function millionths(tokens: number, rate: string): bigint {
|
|
214
|
+
const [whole, frac = ''] = rate.split('.')
|
|
215
|
+
const digits = `${whole}${frac.padEnd(6, '0').slice(0, 6)}`
|
|
216
|
+
const perMillion = BigInt(digits)
|
|
217
|
+
return (BigInt(tokens) * perMillion) / 1_000_000n
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function decimalFromMillionths(value: bigint): UsdDecimal {
|
|
221
|
+
const negative = value < 0n
|
|
222
|
+
const abs = negative ? -value : value
|
|
223
|
+
const whole = abs / 1_000_000n
|
|
224
|
+
const frac = (abs % 1_000_000n).toString().padStart(6, '0').replace(/0+$/, '')
|
|
225
|
+
const text = frac ? `${whole.toString()}.${frac}` : whole.toString()
|
|
226
|
+
return usd(negative ? `-${text}` : text)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type BucketKind = 'required' | 'optional'
|
|
230
|
+
|
|
231
|
+
function priceBucket(
|
|
232
|
+
tokens: number | null,
|
|
233
|
+
rate: string | null | undefined,
|
|
234
|
+
label: string,
|
|
235
|
+
kind: BucketKind,
|
|
236
|
+
parts: bigint[],
|
|
237
|
+
reasons: string[],
|
|
238
|
+
): void {
|
|
239
|
+
if (tokens === null) {
|
|
240
|
+
if (kind === 'required') reasons.push(`${label} tokens unknown`)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (tokens === 0) return
|
|
244
|
+
if (rate === null || rate === undefined) {
|
|
245
|
+
reasons.push(`${label} rate missing`)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
parts.push(millionths(tokens, rate))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function priceUsage(usage: NormalizedUsage, card: PriceCard | undefined): UsagePrice {
|
|
252
|
+
if (!card) {
|
|
253
|
+
return { status: 'unknown', totalUsd: null, observedUsd: null, reasons: ['no price card'] }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const parts: bigint[] = []
|
|
257
|
+
const reasons: string[] = []
|
|
258
|
+
const unreliableBound = usage.inputIncludesCacheRead === true && usage.cacheReadStatus !== 'known'
|
|
259
|
+
if (unreliableBound) {
|
|
260
|
+
reasons.push('input includes cache but the cache split is unknown; observed is not a reliable lower bound')
|
|
261
|
+
}
|
|
262
|
+
priceBucket(usage.uncachedInput, card.uncachedInputPerMillion, 'uncachedInput', 'required', parts, reasons)
|
|
263
|
+
if (usage.cacheReadStatus === 'unknown') {
|
|
264
|
+
reasons.push('cacheRead tokens unknown')
|
|
265
|
+
} else if (usage.cacheReadStatus === 'known') {
|
|
266
|
+
priceBucket(usage.cacheRead, card.cacheReadPerMillion, 'cacheRead', 'optional', parts, reasons)
|
|
267
|
+
}
|
|
268
|
+
priceBucket(usage.output, card.outputPerMillion, 'output', 'required', parts, reasons)
|
|
269
|
+
for (const [ttl, tokens] of Object.entries(usage.cacheWrite)) {
|
|
270
|
+
const status = usage.cacheWriteStatus[ttl] ?? (tokens === null ? 'unknown' : 'known')
|
|
271
|
+
if (status === 'unknown') {
|
|
272
|
+
reasons.push(`cacheWrite.${ttl} tokens unknown`)
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
if (status === 'not_applicable') continue
|
|
276
|
+
priceBucket(tokens, card.cacheWritePerMillion?.[ttl] ?? card.cacheWritePerMillion?.default, `cacheWrite.${ttl}`, 'optional', parts, reasons)
|
|
277
|
+
}
|
|
278
|
+
if (usage.reasoningBilling?.kind === 'separate' || usage.reasoningSeparate !== null) {
|
|
279
|
+
const rate = card.reasoningPerMillion ?? card.outputPerMillion
|
|
280
|
+
priceBucket(usage.reasoningBilling?.tokens ?? usage.reasoningSeparate, rate, 'reasoningSeparate', 'required', parts, reasons)
|
|
281
|
+
}
|
|
282
|
+
if (usage.reasoningBilling?.kind === 'unknown' || (usage.reasoningUnspecified !== null && usage.reasoningUnspecified !== 0)) {
|
|
283
|
+
reasons.push('reasoning contract unspecified; cannot add or omit as a complete total')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const observed = !unreliableBound && parts.length
|
|
287
|
+
? decimalFromMillionths(parts.reduce((sum, part) => sum + part, 0n))
|
|
288
|
+
: (!unreliableBound && !parts.length ? null : null)
|
|
289
|
+
const primaryMissing = usage.uncachedInput === null && usage.output === null && usage.cacheReadStatus !== 'known'
|
|
290
|
+
&& Object.values(usage.cacheWriteStatus).every(status => status !== 'known')
|
|
291
|
+
&& usage.reasoningSeparate === null
|
|
292
|
+
&& usage.reasoningOutputSubset === null
|
|
293
|
+
&& (usage.reasoningUnspecified === null)
|
|
294
|
+
|
|
295
|
+
if (reasons.length && primaryMissing) {
|
|
296
|
+
return { status: 'unknown', totalUsd: null, observedUsd: observed, reasons }
|
|
297
|
+
}
|
|
298
|
+
if (reasons.length) {
|
|
299
|
+
return { status: 'incomplete', totalUsd: null, observedUsd: observed, reasons }
|
|
300
|
+
}
|
|
301
|
+
const total = observed ?? usd('0')
|
|
302
|
+
return { status: 'complete', totalUsd: total, observedUsd: total, reasons: [] }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function apiEquivalentUsd(usage: NormalizedUsage, card: PriceCard | undefined): UsdDecimal | null {
|
|
306
|
+
const priced = priceUsage(usage, card)
|
|
307
|
+
return priced.status === 'complete' ? priced.totalUsd : null
|
|
308
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { digestOf } from '../digest.js'
|
|
2
|
+
import { FusionError, USAGE_LEDGER_REQUIRED } from '../errors.js'
|
|
3
|
+
import {
|
|
4
|
+
projectLedger,
|
|
5
|
+
restoreLedger,
|
|
6
|
+
type Count,
|
|
7
|
+
type UsageLedgerV2,
|
|
8
|
+
} from './ledger.js'
|
|
9
|
+
|
|
10
|
+
export interface PriceRates {
|
|
11
|
+
input?: string
|
|
12
|
+
cachedInput?: string
|
|
13
|
+
output?: string
|
|
14
|
+
/** Explicit separate-reasoning rate; no assumed relationship to output price. */
|
|
15
|
+
reasoning?: string
|
|
16
|
+
cacheWrite?: Readonly<Record<string, string>>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RequestQuote {
|
|
20
|
+
authority: 'none' | 'provisional' | 'final'
|
|
21
|
+
status: 'unknown' | 'provisional' | 'incomplete' | 'complete'
|
|
22
|
+
totalUsd: string | null
|
|
23
|
+
estimateUsd: string | null
|
|
24
|
+
priceCardDigest: string
|
|
25
|
+
missing: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireThat(value: unknown, code: string): asserts value {
|
|
29
|
+
if (!value) throw new FusionError(USAGE_LEDGER_REQUIRED, code)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 18 decimal places in USD: a rate with <=12 decimals per 1e6 tokens is exact. */
|
|
33
|
+
function rateUnits(rate: string): bigint {
|
|
34
|
+
requireThat(/^\d+(?:\.\d{1,12})?$/.test(rate), 'INVALID_PRICE_RATE')
|
|
35
|
+
const [whole, frac = ''] = rate.split('.')
|
|
36
|
+
return BigInt(whole! + frac.padEnd(12, '0'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decimal(units: bigint): string {
|
|
40
|
+
const div = 10n ** 18n
|
|
41
|
+
const fraction = (units % div).toString().padStart(18, '0').replace(/0+$/, '')
|
|
42
|
+
return `${units / div}${fraction ? `.${fraction}` : ''}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function quoteLedger(ledger: UsageLedgerV2, rates: PriceRates, priceCardDigest = digestOf(rates)): RequestQuote {
|
|
46
|
+
const projection = projectLedger(restoreLedger(ledger))
|
|
47
|
+
const bill = projection.bill
|
|
48
|
+
const missing: string[] = []
|
|
49
|
+
let units = 0n
|
|
50
|
+
function add(count: Count, rate: string | undefined, name: string) {
|
|
51
|
+
if (count.state === 'not_applicable') return
|
|
52
|
+
if (count.state === 'unknown') {
|
|
53
|
+
missing.push(`${name}:unknown`)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
requireThat(Number.isSafeInteger(count.tokens) && count.tokens >= 0, 'INVALID_COUNT')
|
|
57
|
+
if (count.tokens === 0) return
|
|
58
|
+
if (rate === undefined) {
|
|
59
|
+
missing.push(`${name}:rate-missing`)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
units += BigInt(count.tokens) * rateUnits(rate)
|
|
63
|
+
}
|
|
64
|
+
if (!bill) {
|
|
65
|
+
return {
|
|
66
|
+
authority: projection.authority,
|
|
67
|
+
status: 'unknown',
|
|
68
|
+
totalUsd: null,
|
|
69
|
+
estimateUsd: null,
|
|
70
|
+
priceCardDigest,
|
|
71
|
+
missing: ['no-bill'],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
add(bill.uncachedInput, rates.input, 'input')
|
|
75
|
+
add(bill.cacheRead, rates.cachedInput, 'cache-read')
|
|
76
|
+
add(bill.output, rates.output, 'output')
|
|
77
|
+
if (bill.cacheWrite.kind === 'unknown') missing.push('cache-write:unknown')
|
|
78
|
+
if (bill.cacheWrite.kind === 'aggregate') add(bill.cacheWrite.tokens, rates.cacheWrite?.[bill.cacheWrite.rateKey], 'cache-write')
|
|
79
|
+
if (bill.cacheWrite.kind === 'details') {
|
|
80
|
+
for (const [key, bucket] of Object.entries(bill.cacheWrite.buckets)) {
|
|
81
|
+
add(bucket.tokens, rates.cacheWrite?.[bucket.rateKey], `cache-write:${key}`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (bill.reasoning.kind === 'unknown') missing.push('reasoning-contract:unknown')
|
|
85
|
+
if (bill.reasoning.kind === 'separate') add(bill.reasoning.tokens, rates.reasoning, 'reasoning')
|
|
86
|
+
const complete = projection.authority === 'final' && missing.length === 0
|
|
87
|
+
return {
|
|
88
|
+
authority: projection.authority,
|
|
89
|
+
status: complete ? 'complete' : projection.authority === 'final' ? 'incomplete' : 'provisional',
|
|
90
|
+
totalUsd: complete ? decimal(units) : null,
|
|
91
|
+
estimateUsd: decimal(units),
|
|
92
|
+
priceCardDigest,
|
|
93
|
+
missing,
|
|
94
|
+
}
|
|
95
|
+
}
|