@ljwei-stak/dsh-model-router 0.24.0 → 0.25.0

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.
@@ -0,0 +1,61 @@
1
+ /** API money and subscription quantities are separate dimensions. */
2
+ import { plannedPackages } from './run-ledger.mjs'
3
+ import { billingPlan } from './subscription-billing.mjs'
4
+ import { toolForProvider } from './official-tool-registry.mjs'
5
+ import { estimateAssignedPackage } from './router.mjs'
6
+
7
+ export function initialFunding(route, { billing = null, officialCli = false, toolId = null,
8
+ loginBillingFor = () => 'unknown', apiKeyFor = () => false, forceApi = false, executionChannel = null } = {}) {
9
+ const id = toolId ?? toolForProvider(route?.provider)?.id
10
+ if (forceApi) return { billing: 'api', route }
11
+ if (officialCli) return { billing: !apiKeyFor(id) && loginBillingFor(id) === 'subscription' ? 'subscription' : 'api', route }
12
+ if (!billing) return { billing: 'api', route }
13
+ const policy = billingPlan(route, billing.routes ?? [])
14
+ if (policy.mode === 'subscription-only') return { billing: 'subscription', route }
15
+ if (policy.mode === 'api-only' || billing.choice === 'api') return { billing: 'api', route: policy.apiRoute ?? route }
16
+ const sub = policy.subscription
17
+ if (!sub) return { billing: 'api', route }
18
+ if (billing.quota?.status?.(sub.key)) return { billing: 'api', route: policy.apiRoute ?? route }
19
+ if (sub.kind === 'plan-key') return { billing: 'subscription', route }
20
+ if (billing.choice === 'subscription') return { billing: 'subscription', route }
21
+ const login = billing.loginBillingFor?.(sub.toolId) ?? loginBillingFor(sub.toolId)
22
+ return { billing: route?.execution === 'api' || executionChannel === 'harness-llm'
23
+ || login !== 'subscription' ? 'api' : 'subscription', route }
24
+ }
25
+
26
+ export function attachPlanAccounting(plan, task, routes = [], context = {}) {
27
+ const entries = plannedPackages(plan, task).map((item, index) => {
28
+ const selected = item.manualAssignment?.kind === 'api' ? item.manualAssignment
29
+ : { provider: item.recommendedProvider, model: item.recommendedModel }
30
+ const route = routes.find(route => route.provider === selected.provider && route.model === selected.model) ?? selected
31
+ const funding = initialFunding(route, { ...context, executionChannel: item.executionChannel ?? plan.executionChannel,
32
+ ...(context.billingChoices?.[item.id] && context.billing ? { billing: { ...context.billing, choice: context.billingChoices[item.id] } } : {}),
33
+ ...(item.manualAssignment?.kind === 'api' ? { forceApi: true } : {}),
34
+ ...(item.manualAssignment?.kind === 'tool' ? { officialCli: true, toolId: item.manualAssignment.toolId } : {}) })
35
+ const resource = plan.costBreakdown?.[index] ?? estimateAssignedPackage(route, item, task, plan.complexity?.band ?? 'balanced')
36
+ const changed = funding.route?.provider !== route.provider || funding.route?.model !== route.model
37
+ const estimate = changed || plan.subscriptionPricedRoutes?.includes(`${route.provider}/${route.model}`)
38
+ ? estimateAssignedPackage(funding.route, item, task, plan.complexity?.band ?? 'balanced').estimatedCost
39
+ : item.estimatedCost ?? null
40
+ return { id: item.id, billing: funding.billing,
41
+ apiEstimatedCostUsd: funding.billing === 'subscription' ? 0 : estimate,
42
+ estimatedUsage: { inputTokens: resource.inputTokens ?? null, outputTokens: resource.outputTokens ?? null } }
43
+ })
44
+ const api = entries.filter(item => item.billing === 'api')
45
+ plan.accounting = { packages: entries, apiExposure: api.length > 0,
46
+ apiEstimatedCostUsd: api.some(item => !Number.isFinite(item.apiEstimatedCostUsd)) ? null
47
+ : api.reduce((sum, item) => sum + item.apiEstimatedCostUsd, 0) }
48
+ return plan.accounting
49
+ }
50
+
51
+ export const accountingEstimates = plan => plan.accounting.packages.map(item => ({ id: item.id,
52
+ billing: item.billing, estimatedCostUsd: item.apiEstimatedCostUsd }))
53
+
54
+ /** Zero marginal API expense for subscription routes; never rewrite real prices. */
55
+ export function planningFundingPrices(routes, context, channelForRoute = () => null) {
56
+ const subscriptionPricedRoutes = routes.filter(route => initialFunding(route, {
57
+ ...context, executionChannel: channelForRoute(route),
58
+ }).billing === 'subscription').map(route => `${route.provider}/${route.model}`)
59
+ return { subscriptionPricedRoutes, pricing: Object.fromEntries(subscriptionPricedRoutes.map(key => [key,
60
+ { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }])) }
61
+ }
@@ -10,7 +10,7 @@ export async function beginExecutionLifecycle({ state, buildRun, packageEstimate
10
10
  const manager = createBudgetReservations({ state })
11
11
  await manager.recoverInterruptedExecutions()
12
12
  const started = new Map(), results = new Map(), completed = new Set()
13
- const estimates = new Map(packageEstimates.map(item => [item.id, item.estimatedCostUsd ?? item.estimatedCost ?? null]))
13
+ const estimates = new Map(packageEstimates.map(item => [item.id, { estimatedCostUsd: item.estimatedCostUsd ?? item.estimatedCost ?? null, billing: item.billing ?? 'api' }]))
14
14
  let workspace = {}, poisoned = null, closed = false
15
15
  const current = (status = 'running') => {
16
16
  const receipts = [...started].map(([id, item]) => results.get(id) ?? {
@@ -24,6 +24,7 @@ export async function beginExecutionLifecycle({ state, buildRun, packageEstimate
24
24
  for (const item of run.packages ?? []) {
25
25
  if (!started.has(item.id) || completed.has(item.id)) continue
26
26
  item.status = 'running'; item.inFlight = true
27
+ if (!results.has(item.id) || results.get(item.id)?.notStarted) item.billing = estimates.get(item.id)?.billing ?? 'api'
27
28
  if (!results.has(item.id)) Object.assign(item, { ran: false, costUsd: null, costSource: 'pending-receipt' })
28
29
  }
29
30
  }
@@ -65,6 +66,7 @@ export async function beginExecutionLifecycle({ state, buildRun, packageEstimate
65
66
  id: initial.id,
66
67
  current,
67
68
  results: () => [...results.values()],
69
+ reservationFor: id => estimates.get(id),
68
70
  activePackageId: () => [...started.keys()].findLast(id => !completed.has(id)),
69
71
  async onCheckpointStart(item) {
70
72
  const previous = started.get(item.id)
@@ -117,9 +119,9 @@ export async function beginExecutionLifecycle({ state, buildRun, packageEstimate
117
119
  },
118
120
  async reserveExtra(id, estimatedCostUsd, { confirmOverBudget: extraConfirmation = false } = {}) {
119
121
  return required(async () => {
120
- const next = new Map(estimates); next.set(id, estimatedCostUsd)
122
+ const next = new Map(estimates); next.set(id, { estimatedCostUsd, billing: 'api' })
121
123
  const changed = await manager.resizeExecution({ ...identity,
122
- packageEstimates: [...next].filter(([key]) => !completed.has(key)).map(([key, value]) => ({ id: key, estimatedCostUsd: value })),
124
+ packageEstimates: [...next].filter(([key]) => !completed.has(key)).map(([key, value]) => ({ id: key, ...value })),
123
125
  ...settings, confirmOverBudget: extraConfirmation === true })
124
126
  if (changed.accepted) { estimates.clear(); for (const [key, value] of next) estimates.set(key, value) }
125
127
  return changed
@@ -515,6 +515,11 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
515
515
  actualModel: reportedModel,
516
516
  ...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
517
517
  ...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
518
+ ...(result.notStarted === true || result.status === 'unsupported' ? { notStarted: true } : {}),
519
+ ...(result.blocked === true ? { blocked: true } : {}),
520
+ ...(['api', 'subscription'].includes(result.billing) ? { billing: result.billing } : {}),
521
+ ...(['configured-api-key', 'process-environment', 'cli-session'].includes(result.credentialSource)
522
+ ? { credentialSource: result.credentialSource } : {}),
518
523
  ...(typeof result.source === 'string' ? { source: result.source } : {}),
519
524
  ...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
520
525
  executionEnvironment,
@@ -18,10 +18,12 @@ import { homedir } from 'node:os'
18
18
  import { dirname, isAbsolute, join, resolve } from 'node:path'
19
19
  import { archiveSpending, sanitizeArchivedSpending } from './run-ledger.mjs'
20
20
  import { sanitizeBudgetReservations } from './budget-reservations.mjs'
21
+ import { archiveSubscriptionUsage, invalidArchivedSubscriptionUsage, invalidSubscriptionAttempts,
22
+ sanitizeArchivedSubscriptionUsage } from './subscription-usage.mjs'
21
23
 
22
24
  export const MAX_RUNS = 200
23
- // Older Hosts must refuse v2 instead of dropping durable execution holds.
24
- const STATE_VERSION = 2
25
+ // Older Hosts must refuse v3 instead of dropping the separate usage archive.
26
+ const STATE_VERSION = 3
25
27
 
26
28
  /** Same DSH home rule as the Host: DSH_HOME (with ~ expansion) or ~/.dsh. */
27
29
  export function resolveStateHome(env = process.env, home = homedir(), cwd = process.cwd()) {
@@ -43,7 +45,10 @@ const LOCK_STALE_MS = 15_000
43
45
  const LOCK_TIMEOUT_MS = 10_000
44
46
 
45
47
  function emptyState() {
46
- return { version: STATE_VERSION, historyIncomplete: false, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], budgetReservations: {}, dynamicData: { liveBench: null, pricing: null, status: {}, revision: '' }, archivedSpending: sanitizeArchivedSpending(), notices: [], terminalSessions: [] }
48
+ return { version: STATE_VERSION, historyIncomplete: false, subscriptionUsageIncomplete: false,
49
+ onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], budgetReservations: {},
50
+ dynamicData: { liveBench: null, pricing: null, status: {}, revision: '' }, archivedSpending: sanitizeArchivedSpending(),
51
+ archivedSubscriptionUsage: sanitizeArchivedSubscriptionUsage(), notices: [], terminalSessions: [] }
47
52
  }
48
53
 
49
54
  function sanitizeNotices(value) {
@@ -63,6 +68,7 @@ function retainRuns(state, maxRuns) {
63
68
  return true
64
69
  })
65
70
  state.archivedSpending = archiveSpending(evicted, state.archivedSpending)
71
+ state.archivedSubscriptionUsage = archiveSubscriptionUsage(evicted, state.archivedSubscriptionUsage)
66
72
  return state
67
73
  }
68
74
 
@@ -103,7 +109,7 @@ function invalidRunSpending(run) {
103
109
  }
104
110
 
105
111
  function sanitize(value, maxRuns) {
106
- if (!value || typeof value !== 'object' || Array.isArray(value) || ![1, STATE_VERSION].includes(value.version)
112
+ if (!value || typeof value !== 'object' || Array.isArray(value) || ![1, 2, STATE_VERSION].includes(value.version)
107
113
  || !Array.isArray(value.runs)) {
108
114
  const error = new Error('模型路由状态版本或结构不受支持;原文件未修改,请先恢复或升级兼容版本。')
109
115
  error.code = 'ROUTER_STATE_UNSUPPORTED'
@@ -119,6 +125,11 @@ function sanitize(value, maxRuns) {
119
125
  || invalidArchivedSpending(value.archivedSpending) || runs.some(invalidRunSpending) || held.invalid
120
126
  || Object.values(held.reservations).some(entry => !runs.some(run => run.id === entry.runId))
121
127
  || sanitizeNotices(value.notices).some(notice => notice.kind === 'state-corrupt'),
128
+ subscriptionUsageIncomplete: value.subscriptionUsageIncomplete === true
129
+ || invalidArchivedSubscriptionUsage(value.archivedSubscriptionUsage)
130
+ || runs.some(run => invalidArchivedSubscriptionUsage(run.priorAttemptSubscriptionUsage)
131
+ || [...(Array.isArray(run.packages) ? run.packages : []), ...(Array.isArray(run.reviews) ? run.reviews : [])]
132
+ .some(item => invalidSubscriptionAttempts(item?.subscriptionAttempts))),
122
133
  onboarding: { completedAt: Number.isFinite(value.onboarding?.completedAt) ? value.onboarding.completedAt : null },
123
134
  health: value.health && Array.isArray(value.health.tools) ? value.health : null,
124
135
  quota: value.quota && typeof value.quota === 'object' && !Array.isArray(value.quota) ? value.quota : {},
@@ -128,6 +139,8 @@ function sanitize(value, maxRuns) {
128
139
  budgetReservations: held.reservations,
129
140
  dynamicData: value.dynamicData && typeof value.dynamicData === 'object' && !Array.isArray(value.dynamicData) ? value.dynamicData : { liveBench: null, pricing: null, status: {}, revision: '' },
130
141
  archivedSpending: sanitizeArchivedSpending(value.archivedSpending),
142
+ archivedSubscriptionUsage: sanitizeArchivedSubscriptionUsage(value.archivedSubscriptionUsage,
143
+ { legacySpending: value.archivedSpending }),
131
144
  notices: sanitizeNotices(value.notices),
132
145
  terminalSessions: sanitizeTerminalSessions(value.terminalSessions),
133
146
  }, maxRuns)
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { reasoningExecutionFields, unsupportedCliReasoning } from './reasoning-execution.mjs'
8
8
  import { packageSelectionFields } from './package-assignments.mjs'
9
+ import { archiveSubscriptionUsage, sanitizeArchivedSubscriptionUsage, sanitizeSubscriptionAttempts } from './subscription-usage.mjs'
9
10
 
10
11
  export const MAX_STORED_ANSWER = 4_000
11
12
  export const MAX_STORED_TASK = 20_000
@@ -128,7 +129,7 @@ export function actualCost(result, pricing) {
128
129
  export function billingOf(result, { loginBilling = null, apiKeyPresent = false } = {}) {
129
130
  if (!result || result.blocked) return null
130
131
  // The subscription-first executor states the channel it billed explicitly.
131
- if (result.billing === 'api' || result.billing === 'subscription') return result.billing
132
+ if (result.billing === 'api' || result.billing === 'subscription' || result.billing === 'unknown') return result.billing
132
133
  if (result.channel !== 'official-cli') return 'api'
133
134
  if (result.credentialSource === 'configured-api-key' || result.credentialSource === 'process-environment') return 'api'
134
135
  if (!result.credentialSource && apiKeyPresent) return 'api'
@@ -137,6 +138,7 @@ export function billingOf(result, { loginBilling = null, apiKeyPresent = false }
137
138
 
138
139
  /** Cost fields for storage: subscription runs keep the figure as `referenceCostUsd` only. */
139
140
  export function billedCost(result, pricing, billing) {
141
+ if (billing === 'unknown') return { costUsd: null, costSource: 'billing-unknown', billing, referenceCostUsd: null }
140
142
  const cost = actualCost(result, pricing)
141
143
  if (billing !== 'subscription') return { ...cost, billing: billing ?? 'api', referenceCostUsd: null }
142
144
  return { costUsd: null, costSource: cost.costSource, billing, referenceCostUsd: cost.costUsd }
@@ -373,6 +375,7 @@ function storedPackage(planned, result, pricing, finishedAt, billingFor = billin
373
375
  ...(result?.billingMode ? { billingMode: String(result.billingMode) } : {}),
374
376
  ...(result?.billingSwitch ? { billingSwitch: { ...result.billingSwitch } } : {}),
375
377
  ...(result?.subscriptionRoute ? { subscriptionRoute: { provider: String(result.subscriptionRoute.provider ?? ''), model: String(result.subscriptionRoute.model ?? '') } } : {}),
378
+ ...(result?.subscriptionAttempts !== undefined ? { subscriptionAttempts: sanitizeSubscriptionAttempts(result.subscriptionAttempts) } : {}),
376
379
  error: result?.ok ? null : (result?.error ?? null),
377
380
  ...(!result?.ok && result?.diagnostic ? { diagnostic: String(result.diagnostic).slice(0, 2_000) } : {}),
378
381
  ...(Number.isInteger(result?.exitCode) ? { exitCode: result.exitCode } : {}),
@@ -443,6 +446,8 @@ export function mergeRerun(run, execution, { rerunIds = [], pricingFor = () => n
443
446
  // Retrying replaces the visible result, but earlier attempts still incurred
444
447
  // charges (or unknown usage). Keep their calendar totals before replacement.
445
448
  const replaced = run.packages.filter(stored => retried.has(stored.id) && results.some(item => item.id === stored.id))
449
+ run.priorAttemptSubscriptionUsage = archiveSubscriptionUsage([{ createdAt: run.createdAt, packages: replaced }],
450
+ sanitizeArchivedSubscriptionUsage(run.priorAttemptSubscriptionUsage, { legacySpending: run.priorAttemptSpending }))
446
451
  run.priorAttemptSpending = archiveSpending([{ createdAt: run.createdAt, packages: replaced }], run.priorAttemptSpending)
447
452
  run.packages = run.packages.map(stored => {
448
453
  const result = results.find(item => item.id === stored.id)
@@ -460,6 +465,7 @@ export function storedResults(run) {
460
465
  id: item.id, name: item.name, ok: item.ok, provider: item.provider, model: item.model,
461
466
  channel: item.channel, answer: item.answer, error: item.error, fallback: item.fallback,
462
467
  blocked: item.blocked, finishedAt: item.finishedAt,
468
+ ...(item.subscriptionAttempts !== undefined ? { subscriptionAttempts: sanitizeSubscriptionAttempts(item.subscriptionAttempts) } : {}),
463
469
  ...packageSelectionFields(item),
464
470
  ...reasoningExecutionFields(item),
465
471
  ...(finite(item.durationMs) && item.durationMs >= 0 ? { durationMs: item.durationMs } : {}),
@@ -483,6 +489,10 @@ export function teamExecutionResults(plan, execution) {
483
489
  id: result.id,
484
490
  ok: result.status === 'succeeded',
485
491
  cancelled: result.status === 'cancelled',
492
+ ...(result.notStarted === true || result.status === 'unsupported' ? { notStarted: true } : {}),
493
+ ...(result.blocked === true ? { blocked: true } : {}),
494
+ ...(result.billing === 'api' || result.billing === 'subscription' || result.billing === 'unknown' ? { billing: result.billing } : {}),
495
+ ...(typeof result.credentialSource === 'string' ? { credentialSource: result.credentialSource } : {}),
486
496
  provider: result.provider,
487
497
  model: result.recommendedModel,
488
498
  ...unsupportedCliReasoning({ reasoningEffort: result.recommendedReasoningEffort }),
@@ -497,6 +507,7 @@ export function teamExecutionResults(plan, execution) {
497
507
  ...(typeof result.source === 'string' ? { source: result.source } : {}),
498
508
  ...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
499
509
  ...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
510
+ ...(result.subscriptionAttempts !== undefined ? { subscriptionAttempts: sanitizeSubscriptionAttempts(result.subscriptionAttempts) } : {}),
500
511
  ...(typeof result.executionEnvironment === 'string' ? { executionEnvironment: result.executionEnvironment } : {}),
501
512
  channel: 'official-cli',
502
513
  toolId: result.toolId ?? null,
@@ -545,6 +556,8 @@ export function buildToolRunRecord({ toolId, toolLabel, provider, model, cliMode
545
556
  id: packageId, ok: succeeded, cancelled: result?.status === 'cancelled',
546
557
  ...(result?.notStarted === true || result?.status === 'unsupported' ? { notStarted: true } : {}),
547
558
  ...(result?.blocked === true ? { blocked: true } : {}),
559
+ ...(result?.billing === 'api' || result?.billing === 'subscription' || result?.billing === 'unknown' ? { billing: result.billing } : {}),
560
+ ...(typeof result?.credentialSource === 'string' ? { credentialSource: result.credentialSource } : {}),
548
561
  provider: plan.directRoute.provider, model: plan.directRoute.model,
549
562
  channel: 'official-cli', toolId,
550
563
  requestedCliModel: Object.hasOwn(result ?? {}, 'requestedModel') ? result.requestedModel : cliModel,
@@ -554,6 +567,7 @@ export function buildToolRunRecord({ toolId, toolLabel, provider, model, cliMode
554
567
  ...(typeof result?.source === 'string' ? { source: result.source } : {}),
555
568
  ...(typeof result?.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
556
569
  ...(result?.usageIncomplete === true ? { usageIncomplete: true } : {}),
570
+ ...(result?.subscriptionAttempts !== undefined ? { subscriptionAttempts: sanitizeSubscriptionAttempts(result.subscriptionAttempts) } : {}),
557
571
  ...(receiptTimestamp(result?.finishedAt) ? { finishedAt: result.finishedAt } : {}),
558
572
  answer: String(result?.finalText ?? ''),
559
573
  error: succeeded ? null : String(result?.error ?? result?.reason ?? result?.status ?? '官方 CLI 未成功完成。'),
@@ -0,0 +1,210 @@
1
+ /** Subscription quantities are evidence of use, never API dollars or remaining quota. */
2
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
3
+ const counter = value => Number.isSafeInteger(value) && value >= 0
4
+ const timestamp = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isFinite(new Date(value).getTime())
5
+ const clean = value => typeof value === 'string' && value.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(value) ? value : null
6
+ const TOKENS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']
7
+ const COUNTS = ['calls', 'knownUsageCalls', 'unknownUsageCalls']
8
+ const pad = value => String(value).padStart(2, '0')
9
+ const dayKey = at => { const d = new Date(at); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` }
10
+ const monthKey = at => { const d = new Date(at); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}` }
11
+ const DAYS = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/u
12
+ const MONTHS = /^\d{4}-(?:0[1-9]|1[0-2])$/u
13
+ export const MAX_SUBSCRIPTION_USAGE_ROUTES = 256
14
+
15
+ export const emptySubscriptionUsage = () => ({ calls: 0, knownUsageCalls: 0, unknownUsageCalls: 0,
16
+ inputTokens: null, outputTokens: null, cacheReadTokens: null, cacheWriteTokens: null })
17
+ const period = value => ({ ...Object.fromEntries(COUNTS.map(field => [field, counter(value?.[field]) ? value[field] : 0])),
18
+ ...Object.fromEntries(TOKENS.map(field => [field, counter(value?.[field]) ? value[field] : null])) })
19
+ const identity = value => ({ provider: clean(value?.provider) || '未知供应商', toolId: clean(value?.toolId), model: clean(value?.model) })
20
+ const keyOf = value => JSON.stringify([value.provider, value.toolId, value.model])
21
+ const emptyBucket = () => ({ total: emptySubscriptionUsage(), byRoute: {} })
22
+ const overflowRoute = { provider: '其他(已合并)', toolId: null, model: null }
23
+
24
+ function addPeriod(target, source) {
25
+ for (const field of COUNTS) target[field] += source[field]
26
+ for (const field of TOKENS) {
27
+ if (source[field] === null) continue
28
+ const total = (target[field] ?? 0) + source[field]
29
+ // A quantity that cannot be represented exactly is not a reported zero.
30
+ target[field] = counter(total) ? total : null
31
+ }
32
+ return target
33
+ }
34
+
35
+ function addRoute(bucket, route, value) {
36
+ let id = keyOf(route)
37
+ if (!Object.hasOwn(bucket.byRoute, id) && Object.keys(bucket.byRoute).length >= MAX_SUBSCRIPTION_USAGE_ROUTES - 1) {
38
+ route = overflowRoute; id = keyOf(route)
39
+ }
40
+ const row = bucket.byRoute[id] ??= { ...route, usage: emptySubscriptionUsage() }
41
+ addPeriod(row.usage, value)
42
+ }
43
+
44
+ function addBucket(target, source) {
45
+ addPeriod(target.total, source.total)
46
+ for (const row of Object.values(source.byRoute)) addRoute(target, identity(row), row.usage)
47
+ }
48
+
49
+ /** Legacy calendar records kept counts, but discarded every token quantity. */
50
+ function legacyArchive(spending) {
51
+ const result = { days: {}, months: {} }
52
+ for (const [kind, pattern] of [['days', DAYS], ['months', MONTHS]]) {
53
+ for (const [key, value] of Object.entries(record(spending?.[kind]) ? spending[kind] : {})) {
54
+ if (!pattern.test(key) || !counter(value?.subscriptionRuns) || value.subscriptionRuns === 0) continue
55
+ const usage = { ...emptySubscriptionUsage(), calls: value.subscriptionRuns, unknownUsageCalls: value.subscriptionRuns }
56
+ const bucket = result[kind][key] = { total: usage, byRoute: {} }
57
+ addRoute(bucket, { provider: '历史订阅(用量未保存)', toolId: null, model: null }, usage)
58
+ }
59
+ }
60
+ return result
61
+ }
62
+
63
+ /** Bounded quantities and route identifiers only; no answers, prompts, prices or credentials. */
64
+ export function sanitizeArchivedSubscriptionUsage(value, { legacySpending } = {}) {
65
+ if (value === undefined || value === null) value = legacyArchive(legacySpending)
66
+ const result = { days: {}, months: {} }
67
+ for (const [kind, pattern, limit] of [['days', DAYS, 90], ['months', MONTHS, 24]]) {
68
+ const entries = Object.entries(record(value?.[kind]) ? value[kind] : {}).filter(([key, bucket]) => pattern.test(key) && record(bucket))
69
+ .sort(([a], [b]) => a.localeCompare(b)).slice(-limit)
70
+ for (const [key, bucket] of entries) {
71
+ const next = result[kind][key] = { total: period(bucket.total), byRoute: {} }
72
+ for (const row of Object.values(record(bucket.byRoute) ? bucket.byRoute : {})) {
73
+ if (!record(row)) continue
74
+ addRoute(next, identity(row), period(row.usage))
75
+ }
76
+ }
77
+ }
78
+ return result
79
+ }
80
+
81
+ export function invalidArchivedSubscriptionUsage(value) {
82
+ if (value === undefined) return false
83
+ if (!record(value)) return true
84
+ const invalidPeriod = value => !record(value)
85
+ || COUNTS.some(field => !counter(value[field]))
86
+ || TOKENS.some(field => value[field] !== null && !counter(value[field]))
87
+ || value.knownUsageCalls + value.unknownUsageCalls !== value.calls
88
+ for (const [kind, pattern] of [['days', DAYS], ['months', MONTHS]]) {
89
+ if (!record(value[kind])) return true
90
+ for (const [key, bucket] of Object.entries(value[kind])) {
91
+ if (!pattern.test(key) || !record(bucket) || invalidPeriod(bucket.total) || !record(bucket.byRoute)) return true
92
+ for (const row of Object.values(bucket.byRoute)) {
93
+ if (!record(row) || !clean(row.provider) || (row.toolId !== null && !clean(row.toolId))
94
+ || (row.model !== null && !clean(row.model)) || invalidPeriod(row.usage)) return true
95
+ }
96
+ }
97
+ }
98
+ return false
99
+ }
100
+
101
+ /** Only these fields from a subscription receipt can enter the durable ledger. */
102
+ export function sanitizeSubscriptionAttempts(value) {
103
+ if (value === undefined) return undefined
104
+ if (!Array.isArray(value)) return [{ usage: null, usageIncomplete: true, unknownEvidence: true }]
105
+ const attempts = value.slice(0, 16).map(item => {
106
+ if (!record(item)) return { usage: null, usageIncomplete: true, unknownEvidence: true }
107
+ return { ...Object.fromEntries(['provider', 'toolId', 'model'].filter(field => Object.hasOwn(item, field))
108
+ .map(field => [field, clean(item[field])])),
109
+ usage: record(item.usage)
110
+ ? Object.fromEntries(TOKENS.filter(field => Object.hasOwn(item.usage, field)).map(field => [field,
111
+ counter(item.usage[field]) ? item.usage[field] : null])) : null,
112
+ ...(item.ran === false ? { ran: false } : {}), ...(item.notStarted === true ? { notStarted: true } : {}),
113
+ ...(item.blocked === true ? { blocked: true } : {}),
114
+ ...(item.usageIncomplete === true || record(item.usage) && TOKENS.some(field => item.usage[field] !== undefined
115
+ && item.usage[field] !== null && !counter(item.usage[field])) ? { usageIncomplete: true } : {}),
116
+ ...(timestamp(item.finishedAt) ? { finishedAt: item.finishedAt } : {}),
117
+ ...(item.unknownEvidence === true ? { unknownEvidence: true } : {}) }
118
+ })
119
+ if (value.length > 16) attempts.push({ usage: null, usageIncomplete: true, unknownEvidence: true })
120
+ return attempts
121
+ }
122
+
123
+ export function invalidSubscriptionAttempts(value) {
124
+ if (value === undefined) return false
125
+ return !Array.isArray(value) || value.length > 16 || value.some(item => !record(item) || item.unknownEvidence === true
126
+ || (item.usage !== undefined && item.usage !== null && (!record(item.usage)
127
+ || TOKENS.some(field => item.usage[field] !== undefined && item.usage[field] !== null && !counter(item.usage[field])))))
128
+ }
129
+
130
+ function began(item) {
131
+ const contradictory = record(item.usage) && TOKENS.some(field => item.usage[field] !== undefined
132
+ && item.usage[field] !== null && item.usage[field] !== 0)
133
+ return contradictory || !(item.ran === false || item.notStarted === true || item.blocked === true)
134
+ }
135
+
136
+ function* packageEntries(item, fallbackAt) {
137
+ if (!record(item)) return
138
+ // An explicit vector is authoritative: an API result can still include its
139
+ // earlier subscription attempt; [] means no subscription was attempted.
140
+ const attempts = Object.hasOwn(item, 'subscriptionAttempts') ? sanitizeSubscriptionAttempts(item.subscriptionAttempts)
141
+ : item.billing === 'subscription' && item.ran === true ? [item] : []
142
+ for (const attempt of attempts) {
143
+ if (!began(attempt)) continue
144
+ const usage = record(attempt.usage) ? attempt.usage : null
145
+ const complete = attempt.usageIncomplete !== true && attempt.unknownEvidence !== true
146
+ && counter(usage?.inputTokens) && counter(usage?.outputTokens)
147
+ && TOKENS.every(field => usage[field] === undefined || usage[field] === null || counter(usage[field]))
148
+ const value = { ...emptySubscriptionUsage(), calls: 1, knownUsageCalls: complete ? 1 : 0, unknownUsageCalls: complete ? 0 : 1 }
149
+ for (const field of TOKENS) if (counter(usage?.[field])) value[field] = usage[field]
150
+ const when = timestamp(attempt.finishedAt) ? attempt.finishedAt : timestamp(item.finishedAt) ? item.finishedAt : fallbackAt
151
+ yield { when, route: identity(Object.fromEntries(['provider', 'toolId', 'model'].map(field => [field,
152
+ Object.hasOwn(attempt, field) ? attempt[field] : item[field]]))), usage: value }
153
+ }
154
+ }
155
+
156
+ function* usageEntries(runs) {
157
+ for (const run of Array.isArray(runs) ? runs : []) {
158
+ for (const item of [...(Array.isArray(run?.packages) ? run.packages : []), ...(Array.isArray(run?.reviews) ? run.reviews : [])]) {
159
+ yield* packageEntries(item, run?.createdAt)
160
+ }
161
+ }
162
+ }
163
+
164
+ function addArchive(target, source) {
165
+ for (const kind of ['days', 'months']) {
166
+ for (const [key, bucket] of Object.entries(source[kind])) addBucket(target[kind][key] ??= emptyBucket(), bucket)
167
+ }
168
+ }
169
+
170
+ function priorArchive(run) {
171
+ return sanitizeArchivedSubscriptionUsage(run?.priorAttemptSubscriptionUsage, { legacySpending: run?.priorAttemptSpending })
172
+ }
173
+
174
+ /** Fold only evicted/replaced records. Retained records remain live. */
175
+ export function archiveSubscriptionUsage(runs, archived) {
176
+ const result = sanitizeArchivedSubscriptionUsage(archived)
177
+ for (const run of Array.isArray(runs) ? runs : []) addArchive(result, priorArchive(run))
178
+ for (const { when, route, usage } of usageEntries(runs)) {
179
+ if (!timestamp(when)) continue
180
+ for (const [kind, key] of [['days', dayKey(when)], ['months', monthKey(when)]]) {
181
+ const bucket = result[kind][key] ??= emptyBucket()
182
+ addPeriod(bucket.total, usage); addRoute(bucket, route, usage)
183
+ }
184
+ }
185
+ return sanitizeArchivedSubscriptionUsage(result)
186
+ }
187
+
188
+ /** Known quantities only. Missing usage remains explicit instead of becoming zero. */
189
+ export function subscriptionUsage(runs, at = Date.now(), archived = null) {
190
+ const all = archiveSubscriptionUsage(runs, archived)
191
+ const daily = all.days[dayKey(at)] ?? emptyBucket(), monthly = all.months[monthKey(at)] ?? emptyBucket()
192
+ const keys = new Set([...Object.keys(daily.byRoute), ...Object.keys(monthly.byRoute)])
193
+ return { today: period(daily.total), month: period(monthly.total),
194
+ byRoute: [...keys].map(key => ({ ...identity(monthly.byRoute[key] ?? daily.byRoute[key]),
195
+ today: period(daily.byRoute[key]?.usage), month: period(monthly.byRoute[key]?.usage) })) }
196
+ }
197
+
198
+ export function subscriptionUsageForPackage(item) {
199
+ const result = emptySubscriptionUsage()
200
+ for (const { usage } of packageEntries(item)) addPeriod(result, usage)
201
+ return result
202
+ }
203
+
204
+ /** Includes prior retry attempts, without summing both daily and monthly copies. */
205
+ export function subscriptionUsageForRun(run) {
206
+ const result = emptySubscriptionUsage()
207
+ for (const bucket of Object.values(priorArchive(run).months)) addPeriod(result, bucket.total)
208
+ for (const { usage } of usageEntries([run])) addPeriod(result, usage)
209
+ return result
210
+ }