segreant 0.1.0 → 0.2.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.
- package/LICENSING.md +5 -3
- package/README.md +57 -19
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +19 -8
- package/dist/alerts/detect.js +13 -0
- package/dist/billing/epistemic.js +4 -0
- package/dist/billing/importer.js +29 -10
- package/dist/billing/reconcile.js +122 -32
- package/dist/billing/scope.js +7 -0
- package/dist/budget/guard.js +33 -4
- package/dist/cli/allocCmd.js +73 -4
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +2 -1
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +68 -9
- package/dist/cli/capitalCmd.js +2 -1
- package/dist/cli/connectCmd.js +50 -9
- package/dist/cli/egressCmd.js +15 -3
- package/dist/cli/evidenceCmd.js +6 -3
- package/dist/cli/flags.js +68 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +210 -54
- package/dist/cli/launchCmd.js +2 -1
- package/dist/cli/opsCmd.js +49 -2
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/runCmd.js +203 -51
- package/dist/cli/showCmd.js +37 -11
- package/dist/cli/teamCmd.js +5 -5
- package/dist/cli/ui.js +24 -8
- package/dist/cli/valueCmd.js +189 -14
- package/dist/cli.js +102 -21
- package/dist/config.js +95 -6
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +43 -3
- package/dist/connect/codex.js +38 -1
- package/dist/connect/importShared.js +58 -5
- package/dist/connect/opencode.js +37 -9
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +4 -1
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +228 -15
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +11 -8
- package/dist/dashboard/contracts.js +23 -3
- package/dist/dashboard/routes.js +980 -112
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +2 -1
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +6 -5
- package/dist/dashboard/web/app/components/drawer.js +105 -35
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +54 -24
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +734 -23
- package/dist/dashboard/web/app/core/api.js +37 -1
- package/dist/dashboard/web/app/core/chain.js +11 -4
- package/dist/dashboard/web/app/core/claimLayers.js +179 -51
- package/dist/dashboard/web/app/core/claimTypes.js +30 -0
- package/dist/dashboard/web/app/core/fmt.js +19 -2
- package/dist/dashboard/web/app/core/generated-contract.js +23 -3
- package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
- package/dist/dashboard/web/app/core/generated-types.js +1 -1
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +79 -46
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +197 -15
- package/dist/dashboard/web/app/views/allocation.js +249 -61
- package/dist/dashboard/web/app/views/control.js +57 -16
- package/dist/dashboard/web/app/views/data.js +4 -4
- package/dist/dashboard/web/app/views/evidence.js +336 -87
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +30 -15
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +44 -4
- package/dist/dashboard/web/app/views/value.js +105 -59
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +16 -4
- package/dist/dashboard/web/index.html +1 -1
- package/dist/dashboard/web/styles/app.css +536 -23
- package/dist/demo/seed.js +153 -14
- package/dist/economics/ledger.js +1 -1
- package/dist/egress/policy.js +129 -5
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +6 -2
- package/dist/export/csv.js +10 -2
- package/dist/export/economic.js +6 -2
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/correlate.js +86 -9
- package/dist/git/quality.js +107 -24
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/guide.js +9 -4
- package/dist/plans/detect.js +147 -0
- package/dist/proxy/server.js +134 -14
- package/dist/proxy/usage.js +9 -4
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/scan/knownApps.js +6 -2
- package/dist/scan/scan.js +9 -0
- package/dist/store/allocation.js +14 -0
- package/dist/store/db.js +259 -34
- package/dist/store/realization.js +78 -14
- package/dist/store/schema.js +59 -1
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/time.js +7 -0
- package/dist/value/realization.js +321 -30
- package/dist/value/report.js +28 -8
- package/dist/value/selfReported.js +10 -9
- package/docs/ARCHITECTURE.md +8 -4
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/DATA-BOUNDARIES.md +66 -6
- package/docs/FAQ.md +43 -9
- package/docs/GETTING-STARTED.md +19 -12
- package/docs/GUIDE.md +81 -18
- package/docs/INTEGRATIONS.md +9 -9
- package/docs/METHODOLOGY.md +3 -2
- package/docs/NEUTRALITY.md +1 -1
- package/docs/PRIVACY.md +51 -0
- package/docs/README.md +1 -0
- package/docs/RELEASE-GATE.md +52 -7
- package/docs/THREAT-MODEL.md +2 -1
- package/package.json +3 -2
- package/pricing/models.json +1366 -60
package/dist/cost/pricing.js
CHANGED
|
@@ -14,9 +14,12 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkS
|
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { egressFetch, EgressError } from '../egress/transport.js';
|
|
17
|
+
import { DEFAULT_PRICE_LIST_URL } from '../egress/policy.js';
|
|
17
18
|
import { dirname, join } from 'node:path';
|
|
18
19
|
import { segreantHome } from '../config.js';
|
|
19
20
|
import { computeExactCost, validateExactModelRate } from './exactPricing.js';
|
|
21
|
+
import { activePriceOverrides, priceOverrideFor } from './overrides.js';
|
|
22
|
+
import { money } from '../economics/money.js';
|
|
20
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
24
|
/** The rate card shipped inside the package — always present, works offline. */
|
|
22
25
|
const BUNDLED_PRICING_PATH = join(__dirname, '..', '..', 'pricing', 'models.json');
|
|
@@ -52,12 +55,14 @@ export const COST_BASES = [
|
|
|
52
55
|
'tool_reported_unverified',
|
|
53
56
|
'synthetic_demo',
|
|
54
57
|
'unpriced',
|
|
58
|
+
'not_sent',
|
|
59
|
+
'user_declared_price',
|
|
55
60
|
'legacy_unknown',
|
|
56
61
|
];
|
|
57
62
|
/** Source of an attached rate-card hash. `none` is deliberately explicit for
|
|
58
63
|
* tool-reported and unpriced rows, so a null hash is never mistaken for a lost
|
|
59
64
|
* local rate card. */
|
|
60
|
-
export const RATE_CARD_SOURCE_KINDS = ['manual', 'native_manifest', 'litellm_transformed', 'bundled', 'legacy_unknown', 'none'];
|
|
65
|
+
export const RATE_CARD_SOURCE_KINDS = ['manual', 'native_manifest', 'litellm_transformed', 'bundled', 'legacy_unknown', 'none', 'user_declaration'];
|
|
61
66
|
/** How the observed model was resolved against a local rate card, if any. */
|
|
62
67
|
export const RATE_MATCH_KINDS = [
|
|
63
68
|
'exact_provider',
|
|
@@ -67,6 +72,9 @@ export const RATE_MATCH_KINDS = [
|
|
|
67
72
|
'fallback',
|
|
68
73
|
'reported',
|
|
69
74
|
'unpriced',
|
|
75
|
+
'not_sent',
|
|
76
|
+
'user_declared',
|
|
77
|
+
'user_declared_free',
|
|
70
78
|
'legacy_unknown',
|
|
71
79
|
];
|
|
72
80
|
export const MAX_PRICING_MANIFEST_BYTES = 5 * 1024 * 1024;
|
|
@@ -75,8 +83,17 @@ let cachedSource = 'bundled';
|
|
|
75
83
|
function sha256(text) {
|
|
76
84
|
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
77
85
|
}
|
|
86
|
+
// Every priced request carries its card's hash, and an import prices tens of
|
|
87
|
+
// thousands of them against one card. A loaded card is replaced, never mutated,
|
|
88
|
+
// when pricing is refreshed, so the hash is remembered per card object.
|
|
89
|
+
const cardHashes = new WeakMap();
|
|
78
90
|
function pricingCardHash(file) {
|
|
79
|
-
|
|
91
|
+
let hash = cardHashes.get(file);
|
|
92
|
+
if (hash === undefined) {
|
|
93
|
+
hash = sha256(JSON.stringify(file));
|
|
94
|
+
cardHashes.set(file, hash);
|
|
95
|
+
}
|
|
96
|
+
return hash;
|
|
80
97
|
}
|
|
81
98
|
function redactedUrl(raw) {
|
|
82
99
|
try {
|
|
@@ -299,15 +316,33 @@ export function loadPricing(force = false) {
|
|
|
299
316
|
* scraping provider pricing pages (brittle HTML that needs a parser which
|
|
300
317
|
* itself rots). The GET fetches a public file and sends nothing about the user.
|
|
301
318
|
*/
|
|
302
|
-
export const DEFAULT_MANIFEST_URL =
|
|
303
|
-
/**
|
|
319
|
+
export const DEFAULT_MANIFEST_URL = DEFAULT_PRICE_LIST_URL;
|
|
320
|
+
/**
|
|
321
|
+
* First-party model makers, mapped onto our table's provider keys. Hosts and
|
|
322
|
+
* resellers are excluded: their rates price the host's route, which a bare
|
|
323
|
+
* model name cannot identify.
|
|
324
|
+
*/
|
|
304
325
|
const LITELLM_PROVIDER_MAP = {
|
|
305
326
|
anthropic: 'anthropic',
|
|
306
327
|
openai: 'openai',
|
|
307
328
|
gemini: 'google',
|
|
329
|
+
cohere_chat: 'cohere',
|
|
330
|
+
dashscope: 'dashscope',
|
|
331
|
+
deepseek: 'deepseek',
|
|
332
|
+
mistral: 'mistral',
|
|
333
|
+
perplexity: 'perplexity',
|
|
334
|
+
xai: 'xai',
|
|
335
|
+
zai: 'zai',
|
|
308
336
|
};
|
|
309
337
|
/** Fewer matched models than this means the feed shape changed — refuse, keep the current table. */
|
|
310
338
|
const MIN_TRANSFORMED_MODELS = 10;
|
|
339
|
+
/** A per-1M rate as a plain decimal string, never exponent notation (1e-7 is "0.0000001"). */
|
|
340
|
+
function plainDecimal(n) {
|
|
341
|
+
const s = String(n);
|
|
342
|
+
if (!/e/i.test(s))
|
|
343
|
+
return s;
|
|
344
|
+
return n.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
|
|
345
|
+
}
|
|
311
346
|
/** Per-token → per-1M, trimmed of float noise (3e-6/token → 3 $/1M exactly). */
|
|
312
347
|
function perMillion(perToken) {
|
|
313
348
|
return Number((perToken * 1e6).toPrecision(10));
|
|
@@ -315,7 +350,7 @@ function perMillion(perToken) {
|
|
|
315
350
|
/**
|
|
316
351
|
* Convert a LiteLLM price file into our manifest schema. Pure and total: bad
|
|
317
352
|
* input yields `{ ok: false }`, never a throw. Only chat-capable entries from
|
|
318
|
-
*
|
|
353
|
+
* first-party makers are carried; `verified` is false because the rates are
|
|
319
354
|
* community-maintained; the conservative `unknown` fallback comes from the
|
|
320
355
|
* CURRENT table, so a feed can never weaken it.
|
|
321
356
|
*/
|
|
@@ -360,18 +395,39 @@ export function transformLiteLLMManifest(rawText) {
|
|
|
360
395
|
if (typeof e.cache_read_input_token_cost === 'number' && e.cache_read_input_token_cost > 0) {
|
|
361
396
|
rate.cache_read = perMillion(e.cache_read_input_token_cost);
|
|
362
397
|
}
|
|
398
|
+
// The decimal companion, so a model learned from the feed reprices to an
|
|
399
|
+
// exact amount: without it `reprice --apply` refused a first-sight model
|
|
400
|
+
// that had been recorded against the bundled card's exact fallback (PR #146 review).
|
|
401
|
+
const exact = {
|
|
402
|
+
input: plainDecimal(rate.input),
|
|
403
|
+
output: plainDecimal(rate.output),
|
|
404
|
+
};
|
|
405
|
+
if (rate.cache_write_5m !== undefined)
|
|
406
|
+
exact.cache_write_5m = plainDecimal(rate.cache_write_5m);
|
|
407
|
+
if (rate.cache_read !== undefined)
|
|
408
|
+
exact.cache_read = plainDecimal(rate.cache_read);
|
|
409
|
+
rate.exact = exact;
|
|
363
410
|
bucket.models[name] = rate;
|
|
364
411
|
}
|
|
365
412
|
const total = Object.values(providers).reduce((n, p) => n + Object.keys(p.models).length, 0);
|
|
366
413
|
if (total < MIN_TRANSFORMED_MODELS) {
|
|
367
414
|
return { ok: false, error: `feed shape not recognized (only ${total} usable models found) — keeping the current table` };
|
|
368
415
|
}
|
|
416
|
+
// Cross-provider lookups use insertion order: keep the big three ahead of
|
|
417
|
+
// other makers on name collisions, regardless of the upstream key order.
|
|
418
|
+
const primaryProviders = ['anthropic', 'openai', 'google'];
|
|
419
|
+
const providerOrder = [
|
|
420
|
+
...primaryProviders,
|
|
421
|
+
...Object.keys(providers).filter((provider) => !primaryProviders.includes(provider)).sort(),
|
|
422
|
+
];
|
|
369
423
|
const file = {
|
|
370
424
|
schema_version: 1,
|
|
371
425
|
updated: new Date().toISOString().slice(0, 10),
|
|
372
426
|
currency: 'USD',
|
|
373
427
|
unit: 'per_million_tokens',
|
|
374
|
-
providers
|
|
428
|
+
providers: Object.fromEntries(providerOrder
|
|
429
|
+
.filter((provider) => providers[provider] !== undefined)
|
|
430
|
+
.map((provider) => [provider, providers[provider]])),
|
|
375
431
|
fallbacks: loadPricing().fallbacks,
|
|
376
432
|
};
|
|
377
433
|
return { ok: true, file };
|
|
@@ -666,17 +722,29 @@ function heuristicMatch(models, model) {
|
|
|
666
722
|
function activeRateCardSourceKind() {
|
|
667
723
|
return cachedSource === 'bundled' ? 'bundled' : cachedProvenance?.sourceKind ?? 'legacy_unknown';
|
|
668
724
|
}
|
|
669
|
-
function resolved(rate, rateMatchKind, rateMatchProvider, rateMatchModel) {
|
|
725
|
+
function resolved(rate, rateMatchKind, rateMatchProvider, rateMatchModel, declarationSha256 = null) {
|
|
670
726
|
// rateFor always loads the active card first, so the card identity and source
|
|
671
727
|
// describe this exact local computation rather than a later refresh.
|
|
672
728
|
const card = loadPricing();
|
|
729
|
+
const declared = rateMatchKind === 'user_declared' || rateMatchKind === 'user_declared_free';
|
|
730
|
+
// A rate or free declaration is priced by the declaration alone: stamping it
|
|
731
|
+
// with the active card's hash made a card refresh look like a price change
|
|
732
|
+
// for requests whose price had not moved (PR #146 review). Only `like`
|
|
733
|
+
// depends on the card, and it names the card model it follows.
|
|
734
|
+
const fromCard = !declared || rateMatchProvider !== null;
|
|
673
735
|
return {
|
|
674
736
|
rate,
|
|
675
|
-
|
|
737
|
+
// A declared price is resolved, not guessed: it is the price the user said
|
|
738
|
+
// this model costs. It keeps its own basis so it is never shown as list price.
|
|
739
|
+
estimated: rateMatchKind !== 'exact_provider' && rateMatchKind !== 'exact_cross_provider' && !declared,
|
|
676
740
|
pricing: {
|
|
677
|
-
costBasis: rateMatchKind === 'fallback' ? 'fallback_estimate' : 'local_list_price',
|
|
678
|
-
|
|
679
|
-
|
|
741
|
+
costBasis: rateMatchKind === 'fallback' ? 'fallback_estimate' : declared ? 'user_declared_price' : 'local_list_price',
|
|
742
|
+
// A rate or free declaration carries a revision of the declaration itself,
|
|
743
|
+
// labelled as the user's, never the card's: a changed price is then a new
|
|
744
|
+
// revision that value comparisons see, and a card refresh is not
|
|
745
|
+
// (PR #146 review).
|
|
746
|
+
rateCardSha256: fromCard ? pricingCardHash(card) : declarationSha256,
|
|
747
|
+
rateCardSourceKind: fromCard ? activeRateCardSourceKind() : declarationSha256 ? 'user_declaration' : 'none',
|
|
680
748
|
rateMatchKind,
|
|
681
749
|
rateMatchProvider,
|
|
682
750
|
rateMatchModel,
|
|
@@ -705,6 +773,22 @@ export function unpricedPricingEvidence() {
|
|
|
705
773
|
rateMatchModel: null,
|
|
706
774
|
};
|
|
707
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* A request Segreant refused before it reached the provider (a budget block, an
|
|
778
|
+
* egress refusal). Its cost is not unknown: nothing was sent, so nothing was
|
|
779
|
+
* charged. Recording it as `unpriced` made every cap refusal read as a missing
|
|
780
|
+
* price ("1 of 835 requests in this window are unpriced").
|
|
781
|
+
*/
|
|
782
|
+
export function notSentPricingEvidence() {
|
|
783
|
+
return {
|
|
784
|
+
costBasis: 'not_sent',
|
|
785
|
+
rateCardSha256: null,
|
|
786
|
+
rateCardSourceKind: 'none',
|
|
787
|
+
rateMatchKind: 'not_sent',
|
|
788
|
+
rateMatchProvider: null,
|
|
789
|
+
rateMatchModel: null,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
708
792
|
/** The only default for records created before per-request pricing lineage existed. */
|
|
709
793
|
export function legacyPricingEvidence() {
|
|
710
794
|
return {
|
|
@@ -720,9 +804,117 @@ export function legacyPricingEvidence() {
|
|
|
720
804
|
export function syntheticPricingEvidence(cost) {
|
|
721
805
|
return { ...cost.pricing, costBasis: 'synthetic_demo' };
|
|
722
806
|
}
|
|
807
|
+
/** The rate-card rate a `like` declaration points at: exact matches only, never another guess. */
|
|
808
|
+
function exactCardRate(pricing, model) {
|
|
809
|
+
for (const p of Object.keys(pricing.providers)) {
|
|
810
|
+
const r = pricing.providers[p]?.models[model];
|
|
811
|
+
if (r)
|
|
812
|
+
return { provider: p, rate: r };
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
/** USD per 1M tokens as a decimal string, back to the card's number form. */
|
|
817
|
+
function declaredRate(o) {
|
|
818
|
+
const exact = {
|
|
819
|
+
input: o.input,
|
|
820
|
+
output: o.output,
|
|
821
|
+
...(o.cacheRead === undefined ? {} : { cache_read: o.cacheRead }),
|
|
822
|
+
// cacheWrite prices both TTLs unless cacheWrite1h names the 1-hour price
|
|
823
|
+
// (the engine and the usage record already tell the two apart).
|
|
824
|
+
...(o.cacheWrite === undefined ? {} : { cache_write_5m: o.cacheWrite, cache_write_1h: o.cacheWrite }),
|
|
825
|
+
...(o.cacheWrite1h === undefined ? {} : { cache_write_1h: o.cacheWrite1h }),
|
|
826
|
+
};
|
|
827
|
+
return {
|
|
828
|
+
input: Number(o.input),
|
|
829
|
+
output: Number(o.output),
|
|
830
|
+
...(o.cacheRead === undefined ? {} : { cache_read: Number(o.cacheRead) }),
|
|
831
|
+
...(o.cacheWrite === undefined ? {} : { cache_write_5m: Number(o.cacheWrite), cache_write_1h: Number(o.cacheWrite) }),
|
|
832
|
+
...(o.cacheWrite1h === undefined ? {} : { cache_write_1h: Number(o.cacheWrite1h) }),
|
|
833
|
+
exact,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/** A stable revision of what was declared (the prices, not when): the same price declared again is the same revision. */
|
|
837
|
+
function declarationRevision(o) {
|
|
838
|
+
const { declaredAt: _when, ...price } = o;
|
|
839
|
+
const canonical = JSON.stringify(Object.keys(price).sort().map((k) => [k, price[k]]));
|
|
840
|
+
return createHash('sha256').update('segreant-declaration:v1\n' + canonical).digest('hex');
|
|
841
|
+
}
|
|
842
|
+
/** The rate with decimal companions for any it lacks, written from its own numbers. */
|
|
843
|
+
function withExactCompanion(rate) {
|
|
844
|
+
if (rate.exact !== undefined)
|
|
845
|
+
return rate;
|
|
846
|
+
const exact = {
|
|
847
|
+
input: plainDecimal(rate.input),
|
|
848
|
+
output: plainDecimal(rate.output),
|
|
849
|
+
};
|
|
850
|
+
if (rate.cache_write_5m !== undefined)
|
|
851
|
+
exact.cache_write_5m = plainDecimal(rate.cache_write_5m);
|
|
852
|
+
if (rate.cache_write_1h !== undefined)
|
|
853
|
+
exact.cache_write_1h = plainDecimal(rate.cache_write_1h);
|
|
854
|
+
if (rate.cache_read !== undefined)
|
|
855
|
+
exact.cache_read = plainDecimal(rate.cache_read);
|
|
856
|
+
return { ...rate, exact };
|
|
857
|
+
}
|
|
858
|
+
/** A free (local) model: every rate is zero, cache rates included. */
|
|
859
|
+
/** Where rateFor reads declarations; null reads the file (re-read when it changes). */
|
|
860
|
+
let declarationSource = null;
|
|
861
|
+
function withDeclarationSource(source, fn) {
|
|
862
|
+
const previous = declarationSource;
|
|
863
|
+
declarationSource = source;
|
|
864
|
+
try {
|
|
865
|
+
return fn();
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
declarationSource = previous;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Run `fn` pricing from the card alone. The demo is fixed sample data: a real
|
|
873
|
+
* declaration (say, gpt-4o marked free) must not change its spend, evidence or
|
|
874
|
+
* comparisons (PR #146 review).
|
|
875
|
+
*/
|
|
876
|
+
export function withoutDeclaredPrices(fn) {
|
|
877
|
+
return withDeclarationSource(() => null, fn);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Run `fn` against one read of the declarations. A plan priced row by row
|
|
881
|
+
* re-read the file at each row, so a `pricing set` from another process midway
|
|
882
|
+
* gave earlier and later rows of the same model different prices in one
|
|
883
|
+
* correction (PR #146 review). The card needs no snapshot: it is cached for
|
|
884
|
+
* the life of the process and changes only by a refresh in this process,
|
|
885
|
+
* which cannot run inside a synchronous plan.
|
|
886
|
+
*/
|
|
887
|
+
/** Run `fn` against declarations read earlier (a proxied request keeps the ones it was checked against). */
|
|
888
|
+
export function withDeclarations(models, fn) {
|
|
889
|
+
return withDeclarationSource((model) => models[model] ?? null, fn);
|
|
890
|
+
}
|
|
891
|
+
export function withPricingSnapshot(fn) {
|
|
892
|
+
if (declarationSource !== null)
|
|
893
|
+
return fn();
|
|
894
|
+
const models = activePriceOverrides();
|
|
895
|
+
return withDeclarationSource((model) => models[model] ?? null, fn);
|
|
896
|
+
}
|
|
897
|
+
const FREE_RATE = Object.freeze({ input: 0, output: 0, cache_read: 0, cache_write_5m: 0, cache_write_1h: 0 });
|
|
723
898
|
export function rateFor(provider, model) {
|
|
724
899
|
const pricing = loadPricing();
|
|
725
900
|
const here = pricing.providers[provider]?.models;
|
|
901
|
+
// 0) A price the user declared for this exact model name wins: it is the one
|
|
902
|
+
// place a private, fine-tuned or local model can get a real price. A `like`
|
|
903
|
+
// that no longer resolves exactly on the card is ignored, so the request
|
|
904
|
+
// falls to the ordinary ladder rather than to a guess built on a guess.
|
|
905
|
+
const declared = declarationSource ? declarationSource(model) : priceOverrideFor(model);
|
|
906
|
+
if (declared?.kind === 'free')
|
|
907
|
+
return resolved(FREE_RATE, 'user_declared_free', null, model, declarationRevision(declared));
|
|
908
|
+
if (declared?.kind === 'rate')
|
|
909
|
+
return resolved(declaredRate(declared), 'user_declared', null, model, declarationRevision(declared));
|
|
910
|
+
if (declared?.kind === 'like') {
|
|
911
|
+
const target = exactCardRate(pricing, declared.like);
|
|
912
|
+
// A card entry without decimal companions (a hand-written manifest may omit
|
|
913
|
+
// them) would leave a reprice with no exact replacement and roll it back
|
|
914
|
+
// (PR #146 review); the companion is the card's own stated numbers.
|
|
915
|
+
if (target)
|
|
916
|
+
return resolved(withExactCompanion(target.rate), 'user_declared', target.provider, declared.like);
|
|
917
|
+
}
|
|
726
918
|
// 1) Exact id in the named provider — the price we're surest about.
|
|
727
919
|
const exact = here?.[model];
|
|
728
920
|
if (exact)
|
|
@@ -755,11 +947,29 @@ export function rateFor(provider, model) {
|
|
|
755
947
|
export function exactRateFromModelRate(rate) {
|
|
756
948
|
return rate.exact === undefined ? null : validateExactModelRate(rate.exact);
|
|
757
949
|
}
|
|
950
|
+
function zeroExactCost() {
|
|
951
|
+
const zero = money('0', 'USD', 'estimated');
|
|
952
|
+
return Object.freeze({ total: zero, components: Object.freeze({ input: zero, output: zero, cacheWrite: zero, cacheRead: zero }) });
|
|
953
|
+
}
|
|
758
954
|
function per(tokens, rate) {
|
|
759
955
|
if (!tokens || !rate)
|
|
760
956
|
return 0;
|
|
761
957
|
return (tokens / 1_000_000) * rate;
|
|
762
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* The duration a cache write is priced at, and whether the source reported a
|
|
961
|
+
* breakdown. No breakdown: no duration (priced at the standard 5-minute rate,
|
|
962
|
+
* recorded as not recorded). One duration only: that one. Both: the larger
|
|
963
|
+
* share, which is what the request is priced with, and the ledger records it
|
|
964
|
+
* so a later reprice prices it the same way (PR #150 review). A missing
|
|
965
|
+
* breakdown used to be stored as '1h' by the Claude Code import and '5m' by
|
|
966
|
+
* the proxy, each as if it were seen (independent review, 2026-10-18).
|
|
967
|
+
*/
|
|
968
|
+
export function cacheWriteDuration(ephemeral5m, ephemeral1h) {
|
|
969
|
+
if (ephemeral5m <= 0 && ephemeral1h <= 0)
|
|
970
|
+
return { observed: false };
|
|
971
|
+
return { ttl: ephemeral1h > ephemeral5m ? '1h' : '5m', observed: true };
|
|
972
|
+
}
|
|
763
973
|
export function computeCost(provider, model, usage) {
|
|
764
974
|
const { rate, estimated, pricing } = rateFor(provider, model);
|
|
765
975
|
// Cache-write rate: prefer the TTL-specific rate, else default to 1.25x input
|
|
@@ -776,9 +986,12 @@ export function computeCost(provider, model, usage) {
|
|
|
776
986
|
cacheRead: per(usage.cacheReadTokens, readRate),
|
|
777
987
|
};
|
|
778
988
|
const costUsd = components.input + components.output + components.cacheWrite + components.cacheRead;
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
989
|
+
const declared = pricing.rateMatchKind === 'user_declared' || pricing.rateMatchKind === 'user_declared_free';
|
|
990
|
+
const exactRate = rate === FREE_RATE ? null : exactRateFromModelRate(rate);
|
|
991
|
+
const exact = rate === FREE_RATE
|
|
992
|
+
? zeroExactCost()
|
|
993
|
+
: exactRate === null
|
|
994
|
+
? undefined
|
|
995
|
+
: computeExactCost(exactRate, usage, estimated || declared ? 'estimated' : 'list');
|
|
783
996
|
return { costUsd, estimated, pricing, rate, components, ...(exact === undefined ? {} : { exact }) };
|
|
784
997
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { computeCost, withPricingSnapshot } from './pricing.js';
|
|
2
|
+
import { requestEconomicEventId } from '../economics/request.js';
|
|
3
|
+
/**
|
|
4
|
+
* What priced the replacement amounts, from every row that will change: a
|
|
5
|
+
* report built from the first row alone claimed "local rate card only" for a
|
|
6
|
+
* batch that also held prices the user typed (PR #146 review).
|
|
7
|
+
*/
|
|
8
|
+
function repriceEvidence(updates) {
|
|
9
|
+
if (updates.length === 0)
|
|
10
|
+
return null;
|
|
11
|
+
const sources = new Map();
|
|
12
|
+
for (const u of updates) {
|
|
13
|
+
const key = JSON.stringify([u.pricing.costBasis, u.pricing.rateCardSha256, u.pricing.rateCardSourceKind]);
|
|
14
|
+
const s = sources.get(key) ?? { costBasis: u.pricing.costBasis, rateCardSha256: u.pricing.rateCardSha256, rateCardSourceKind: u.pricing.rateCardSourceKind, rows: 0 };
|
|
15
|
+
s.rows += 1;
|
|
16
|
+
sources.set(key, s);
|
|
17
|
+
}
|
|
18
|
+
const list = [...sources.values()].sort((a, b) => b.rows - a.rows);
|
|
19
|
+
const declaredRows = list.filter((s) => s.costBasis === 'user_declared_price').reduce((n, s) => n + s.rows, 0);
|
|
20
|
+
const cards = [...new Set(list.filter((s) => s.rateCardSha256 !== null).map((s) => `${s.rateCardSourceKind} ${s.rateCardSha256}`))];
|
|
21
|
+
const onlyCard = declaredRows === 0 && cards.length === 1 ? list.find((s) => s.rateCardSha256 !== null) : null;
|
|
22
|
+
return {
|
|
23
|
+
localRateCardOnly: declaredRows === 0,
|
|
24
|
+
cardSha256: onlyCard?.rateCardSha256 ?? null,
|
|
25
|
+
sourceKind: onlyCard?.rateCardSourceKind ?? (declaredRows === updates.length ? 'none' : 'mixed'),
|
|
26
|
+
declaredRows,
|
|
27
|
+
sources: list,
|
|
28
|
+
note: declaredRows === 0
|
|
29
|
+
? 'Local list-price estimate only; never provider invoice, discount, tax, credit, or reconciliation.'
|
|
30
|
+
: `${declaredRows} of ${updates.length} rows take a price you set (your price, not a list price); the rest are local list prices. None is a provider invoice, discount, tax, credit, or reconciliation.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function previewReprice(store) {
|
|
34
|
+
// Every row of one plan is priced from the same declarations.
|
|
35
|
+
return withPricingSnapshot(() => planReprice(store));
|
|
36
|
+
}
|
|
37
|
+
function planReprice(store) {
|
|
38
|
+
const rows = store.estimatedRequestRows();
|
|
39
|
+
const updates = [];
|
|
40
|
+
const meta = new Map();
|
|
41
|
+
let stillEstimated = 0;
|
|
42
|
+
// Rows with cache writes whose duration was never recorded (before Segreant
|
|
43
|
+
// kept it): priced at the 5-minute rate, as they were first priced, and
|
|
44
|
+
// counted so the preview says so (PR #146 review).
|
|
45
|
+
let cacheDurationUnknown = 0;
|
|
46
|
+
for (const r of rows) {
|
|
47
|
+
// No captured usage: nothing to price it from, so it stays an estimate.
|
|
48
|
+
if (!r.usageCaptured) {
|
|
49
|
+
stillEstimated++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const c = computeCost(r.provider, r.model, {
|
|
53
|
+
inputTokens: r.inputTokens, outputTokens: r.outputTokens,
|
|
54
|
+
cacheWriteTokens: r.cacheWriteTokens, cacheReadTokens: r.cacheReadTokens,
|
|
55
|
+
...(r.cacheWriteTtl === null ? {} : { cacheWriteTtl: r.cacheWriteTtl }),
|
|
56
|
+
});
|
|
57
|
+
if (c.estimated) {
|
|
58
|
+
stillEstimated++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// A row with an exact economic event can only be corrected by an exact
|
|
62
|
+
// amount. A custom card may omit the decimal companions, and the plan used
|
|
63
|
+
// to promise such a row, then --apply refused it and rolled back the whole
|
|
64
|
+
// batch (PR #146 review). It stays an estimate until a card can price it exactly.
|
|
65
|
+
if (c.exact === undefined && store.economic().read(requestEconomicEventId(r.requestId)) !== null) {
|
|
66
|
+
stillEstimated++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Counted only for rows the plan will rewrite, so the preview never says a
|
|
70
|
+
// row's writes are priced at the 5-minute rate when it is left untouched.
|
|
71
|
+
if (r.cacheWriteTokens > 0 && r.cacheWriteTtl === null)
|
|
72
|
+
cacheDurationUnknown++;
|
|
73
|
+
updates.push({ requestId: r.requestId, costUsd: c.costUsd, pricing: c.pricing, ...(c.exact === undefined ? {} : { economicAmount: c.exact.total }) });
|
|
74
|
+
meta.set(r.requestId, {
|
|
75
|
+
provider: r.provider, model: r.model, beforeUsd: r.costUsd,
|
|
76
|
+
cacheDurationUnknown: r.cacheWriteTokens > 0 && r.cacheWriteTtl === null,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { ...summarize(updates, meta, 0), estimatedRows: rows.length, stillEstimated, cacheDurationUnknown, meta };
|
|
80
|
+
}
|
|
81
|
+
/** Totals, models and evidence for exactly these updates: the plan, or the rows a write touched. */
|
|
82
|
+
function summarize(updates, meta, alreadyRepriced) {
|
|
83
|
+
const byModel = new Map();
|
|
84
|
+
for (const u of updates) {
|
|
85
|
+
const r = meta.get(u.requestId);
|
|
86
|
+
const key = JSON.stringify([r.provider, r.model]);
|
|
87
|
+
const agg = byModel.get(key) ?? {
|
|
88
|
+
provider: r.provider, model: r.model, rows: 0, beforeUsd: 0, afterUsd: 0,
|
|
89
|
+
afterBasis: u.pricing.rateMatchKind === 'user_declared' || u.pricing.rateMatchKind === 'user_declared_free' ? 'your price' : 'list price',
|
|
90
|
+
};
|
|
91
|
+
agg.rows++;
|
|
92
|
+
agg.beforeUsd += r.beforeUsd;
|
|
93
|
+
agg.afterUsd += u.costUsd;
|
|
94
|
+
byModel.set(key, agg);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
applied: false, repriceable: updates.length,
|
|
98
|
+
/** Planned rows another reprice had already rewritten by the time this one wrote: left alone, not counted. */
|
|
99
|
+
alreadyRepriced,
|
|
100
|
+
totalBeforeUsd: [...byModel.values()].reduce((a, v) => a + v.beforeUsd, 0),
|
|
101
|
+
totalAfterUsd: [...byModel.values()].reduce((a, v) => a + v.afterUsd, 0),
|
|
102
|
+
priceEvidence: repriceEvidence(updates),
|
|
103
|
+
byModel: [...byModel.values()],
|
|
104
|
+
// Internal write plan; never expose the exact bigint money objects on the JSON wire.
|
|
105
|
+
updates,
|
|
106
|
+
realizationSync: null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compute once, write that plan through the Store's audited transaction, and
|
|
111
|
+
* report only the rows the transaction wrote. Another process may reprice the
|
|
112
|
+
* same rows between the plan and the write; the store skips a row that is no
|
|
113
|
+
* longer an estimate, and the result used to claim it anyway (PR #146 review).
|
|
114
|
+
*/
|
|
115
|
+
export function applyReprice(store) {
|
|
116
|
+
const preview = previewReprice(store);
|
|
117
|
+
if (preview.updates.length === 0)
|
|
118
|
+
return { ...preview, applied: true };
|
|
119
|
+
const { writtenRequestIds, ...realizationSync } = store.applyRepricedCosts(preview.updates);
|
|
120
|
+
const written = new Set(writtenRequestIds);
|
|
121
|
+
const done = preview.updates.filter((u) => written.has(u.requestId));
|
|
122
|
+
const cacheDurationUnknown = done.filter((u) => preview.meta.get(u.requestId).cacheDurationUnknown).length;
|
|
123
|
+
return {
|
|
124
|
+
...preview,
|
|
125
|
+
...summarize(done, preview.meta, preview.updates.length - done.length),
|
|
126
|
+
cacheDurationUnknown,
|
|
127
|
+
applied: true,
|
|
128
|
+
realizationSync,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* conflict lands on `coverage`, and the difference is the point rather than an
|
|
53
53
|
* inconsistency.
|
|
54
54
|
*/
|
|
55
|
+
import { recordedTime } from '../util/time.js';
|
|
55
56
|
/**
|
|
56
57
|
* The seven axes that do not vary, and why stating them is the point.
|
|
57
58
|
*
|
|
@@ -136,7 +137,7 @@ export function meteredClaimSupport(input) {
|
|
|
136
137
|
notes.push('no priced spend in this window, so pricing coverage is unevidenced rather than complete');
|
|
137
138
|
if (truncated) {
|
|
138
139
|
const removed = input.retention.rowsRemoved === 1 ? '1 request row' : `${input.retention.rowsRemoved} request rows`;
|
|
139
|
-
notes.push(`retention deleted ${removed} from before ${
|
|
140
|
+
notes.push(`retention deleted ${removed} from before ${recordedTime(input.retention.prunedBeforeMs)}, `
|
|
140
141
|
+ 'so this window covers what survived rather than everything metered in it');
|
|
141
142
|
}
|
|
142
143
|
return claimSupportPayload({
|
|
@@ -189,8 +190,8 @@ export function billedClaimSupport(input) {
|
|
|
189
190
|
// deleted by retention bounds nothing either, for a different reason -- the
|
|
190
191
|
// local total is a known undercount by an unknown amount. Both are "this
|
|
191
192
|
// residual establishes no bound", which is what this variable feeds.
|
|
192
|
-
const
|
|
193
|
-
|
|
193
|
+
const truncatedByRetention = input.latest?.offPathBound === 'unknown_local_total_truncated_by_retention';
|
|
194
|
+
const residualBoundsNothing = input.latest?.offPathBound === 'none_local_estimate_exceeds_provider';
|
|
194
195
|
return claimSupportPayload({
|
|
195
196
|
...PRODUCT_CLAIM_AXES,
|
|
196
197
|
epistemic: unstable ? 'conflicted' : 'supported',
|
|
@@ -198,16 +199,18 @@ export function billedClaimSupport(input) {
|
|
|
198
199
|
// immutable and identified by the digest of the result it describes, so it
|
|
199
200
|
// cannot outlive a change to that result.
|
|
200
201
|
integrity: 'verified',
|
|
201
|
-
coverage:
|
|
202
|
+
coverage: truncatedByRetention || residualBoundsNothing ? 'partial' : 'complete',
|
|
202
203
|
monetaryBasis: 'billed',
|
|
203
204
|
},
|
|
204
205
|
// An evidence claim about whether a comparison happened, not a second cost
|
|
205
206
|
// figure. The band carries no dollar in any branch.
|
|
206
207
|
'not_a_money_claim', unstable
|
|
207
208
|
? `provider snapshots disagreed on ${days.length} day(s) of this scope`
|
|
208
|
-
:
|
|
209
|
-
? '
|
|
210
|
-
:
|
|
209
|
+
: truncatedByRetention
|
|
210
|
+
? 'retention deleted local request rows in this period, so the residual bounds no off-path spend'
|
|
211
|
+
: residualBoundsNothing
|
|
212
|
+
? 'the local estimate exceeds the provider total, so the residual bounds no off-path spend'
|
|
213
|
+
: undefined);
|
|
211
214
|
}
|
|
212
215
|
/**
|
|
213
216
|
* Allocation is showback: the claim is whose cost it is, not how much, so the
|
|
@@ -263,6 +266,6 @@ export function realizedClaimSupport(input) {
|
|
|
263
266
|
}, !supported ? 'withheld_unsupported' : input.valued ? 'shown' : 'withheld_uncosted', conflicted.length > 0
|
|
264
267
|
? `${conflictedUnits} mature unit(s) hold contradicted gate evidence at ${conflicted
|
|
265
268
|
.map(([gate]) => gate)
|
|
266
|
-
.join(', ')}
|
|
269
|
+
.join(', ')}; the conflict is unresolved, not a fail`
|
|
267
270
|
: undefined);
|
|
268
271
|
}
|
|
@@ -18,9 +18,19 @@ export const DASHBOARD_API_CONTRACTS = [
|
|
|
18
18
|
{ id: 'scan', path: '/api/scan', methods: ['GET', 'POST'], localOnly: ['POST'], responseType: 'ScanPayload', browserBinding: ['modern-api', 'classic'] },
|
|
19
19
|
{ id: 'overview', path: '/api/overview', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'Overview', browserBinding: ['modern-api', 'classic'] },
|
|
20
20
|
{ id: 'billing', path: '/api/billing', methods: ['GET'], localOnly: [], responseType: 'BillingPayload', browserBinding: ['modern-api', 'classic'] },
|
|
21
|
+
{ id: 'statement', path: '/api/statement', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'StatementPayload', browserBinding: ['modern-api'] },
|
|
22
|
+
{ id: 'sessions', path: '/api/sessions', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'SessionsPayload', browserBinding: ['modern-api'] },
|
|
23
|
+
{ id: 'statement-csv', path: '/api/statement.csv', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'text/csv', browserBinding: ['modern-api'] },
|
|
21
24
|
{ id: 'allocation', path: '/api/allocation', methods: ['GET'], localOnly: [], responseType: 'AllocationPayload', browserBinding: ['modern-api', 'classic'] },
|
|
25
|
+
{ id: 'allocation-write', path: '/api/allocation/write', methods: ['POST'], localOnly: ['POST'], responseType: 'AllocationWritePayload', browserBinding: ['modern-api'] },
|
|
26
|
+
{ id: 'allocation-run-csv', path: '/api/allocation/run.csv', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'text/csv', browserBinding: ['modern-api'] },
|
|
27
|
+
{ id: 'billing-write', path: '/api/billing/write', methods: ['POST'], localOnly: ['POST'], responseType: 'BillingWritePayload', browserBinding: ['modern-api'] },
|
|
22
28
|
{ id: 'economic', path: '/api/economic', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'EconomicPayload', browserBinding: ['modern-api'] },
|
|
23
29
|
{ id: 'pricing', path: '/api/pricing', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'PricingPayload', browserBinding: [] },
|
|
30
|
+
{ id: 'pricing-unknown', path: '/api/pricing/unknown', methods: ['GET'], localOnly: [], responseType: 'UnknownPricingPayload', browserBinding: ['modern-api'] },
|
|
31
|
+
{ id: 'pricing-declare', path: '/api/pricing/declare', methods: ['POST'], localOnly: ['POST'], responseType: 'PricingDeclarePayload', browserBinding: ['modern-api'] },
|
|
32
|
+
{ id: 'pricing-unset', path: '/api/pricing/unset', methods: ['POST'], localOnly: ['POST'], responseType: 'PricingUnsetPayload', browserBinding: ['modern-api'] },
|
|
33
|
+
{ id: 'reprice', path: '/api/reprice', methods: ['POST'], localOnly: ['POST'], responseType: 'RepricePayload', browserBinding: ['modern-api'] },
|
|
24
34
|
{ id: 'export-csv', path: '/api/export.csv', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'text/csv', browserBinding: ['classic', 'actions'] },
|
|
25
35
|
{ id: 'realization', path: '/api/realization', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'RealizationPayload', browserBinding: [] },
|
|
26
36
|
{ id: 'guide', path: '/api/guide', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'GuidePayload', browserBinding: ['modern-api', 'classic'] },
|
|
@@ -59,21 +69,31 @@ export const DASHBOARD_PAYLOAD_CONTRACTS = [
|
|
|
59
69
|
{ routeId: 'scan', method: 'GET', contentType: 'json', responseType: 'ScanPayload', required: [field('ok', 'boolean'), field('tools', 'array'), field('otherApps', 'array'), field('roots', 'array'), field('repoCount', 'number'), field('reposWithSpend', 'number'), field('hitBudget', 'boolean'), field('dirsVisited', 'number'), field('unreadableDirs', 'number'), field('diff', 'object')] },
|
|
60
70
|
{ routeId: 'scan', method: 'POST', contentType: 'json', responseType: 'ScanSetupPayload', required: [field('ok', 'boolean'), field('totalNew', 'number'), field('imported', 'object'), field('correlated', 'number'), field('discovered', 'array')] },
|
|
61
71
|
{ routeId: 'overview', method: 'GET', contentType: 'json', responseType: 'Overview', required: [field('claimSupport', 'object'), field('range', 'string'), field('demo', 'boolean'), field('generatedAt', 'string'), field('budget', 'object'), field('summary', 'object'), field('pricing', 'object'), field('byModel', 'array'), field('byProject', 'array'), field('attributionEvidence', 'array'), field('byUser', 'array'), field('bySource', 'array'), field('characterization', 'object'), field('dimensions', 'array'), field('series', 'array'), field('recent', 'array'), field('alerts', 'array'), field('alertCoverage', 'object')] },
|
|
72
|
+
{ routeId: 'statement', method: 'GET', contentType: 'json', responseType: 'StatementPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('month', 'string'), field('period', 'object'), field('metered', 'object'), field('billed', 'object'), field('allocated', 'object'), field('realized', 'object'), field('close', 'object')] },
|
|
73
|
+
{ routeId: 'sessions', method: 'GET', contentType: 'json', responseType: 'SessionsPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('range', 'string'), field('window', 'object'), field('totals', 'object'), field('models', 'array'), field('buckets', 'array'), field('sessions', 'array'), field('sessionLimit', 'number'), field('sessionsOmitted', 'number'), field('outcomes', 'object')] },
|
|
62
74
|
{ routeId: 'billing', method: 'GET', contentType: 'json', responseType: 'BillingPayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('generatedAt', 'string'), field('evidence', 'object'), field('summary', 'object'), field('imports', 'array'), field('kernel', 'object'), field('readiness', 'object'), field('mapping', 'object'), field('directOpenAiCosts', 'object'), field('reconciliation', 'object')] },
|
|
63
75
|
{ routeId: 'allocation', method: 'GET', contentType: 'json', responseType: 'AllocationPayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('generatedAt', 'string'), field('kind', 'string'), field('trust', 'string'), field('basis', 'string'), field('excludedFrom', 'array'), field('costCentres', 'array'), field('rules', 'array'), field('runs', 'array'), field('reconciliation', 'object')] },
|
|
76
|
+
{ routeId: 'allocation-write', method: 'POST', contentType: 'json', responseType: 'AllocationWritePayload', required: [field('ok', 'boolean'), field('op', 'string'), field('applied', 'boolean')] },
|
|
77
|
+
{ routeId: 'billing-write', method: 'POST', contentType: 'json', responseType: 'BillingWritePayload', required: [field('ok', 'boolean'), field('op', 'string'), field('applied', 'boolean')] },
|
|
64
78
|
{ routeId: 'economic', method: 'GET', contentType: 'json', responseType: 'EconomicPayload', required: [field('kind', 'string'), field('schemaVersion', 'number'), field('demo', 'boolean'), field('window', 'object'), field('translation', 'object', true), field('projection', 'object'), field('periodClose', 'object')] },
|
|
65
79
|
{ routeId: 'pricing', method: 'GET', contentType: 'json', responseType: 'PricingPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('window', 'object'), field('activeRateCard', 'object'), field('total', 'object'), field('provenance', 'array'), field('boundary', 'string')] },
|
|
80
|
+
{ routeId: 'pricing-unknown', method: 'GET', contentType: 'json', responseType: 'UnknownPricingPayload', required: [field('models', 'array'), field('boundary', 'string'), field('demo', 'boolean')] },
|
|
81
|
+
{ routeId: 'pricing-declare', method: 'POST', contentType: 'json', responseType: 'PricingDeclarePayload', required: [field('ok', 'boolean'), field('model', 'string'), field('price', 'object')] },
|
|
82
|
+
{ routeId: 'pricing-unset', method: 'POST', contentType: 'json', responseType: 'PricingUnsetPayload', required: [field('ok', 'boolean'), field('model', 'string'), field('removed', 'boolean')] },
|
|
83
|
+
{ routeId: 'reprice', method: 'POST', contentType: 'json', responseType: 'RepricePayload', required: [field('applied', 'boolean'), field('demo', 'boolean'), field('estimatedRows', 'number'), field('repriceable', 'number'), field('stillEstimated', 'number'), field('totalBeforeUsd', 'number'), field('totalAfterUsd', 'number'), field('byModel', 'array'), field('realizationSync', 'object', true)] },
|
|
66
84
|
{ routeId: 'export-csv', method: 'GET', contentType: 'text', responseType: 'text/csv', required: [] },
|
|
85
|
+
{ routeId: 'statement-csv', method: 'GET', contentType: 'text', responseType: 'text/csv', required: [] },
|
|
86
|
+
{ routeId: 'allocation-run-csv', method: 'GET', contentType: 'text', responseType: 'text/csv', required: [] },
|
|
67
87
|
{ routeId: 'realization', method: 'GET', contentType: 'json', responseType: 'RealizationPayload', required: [field('available', 'boolean'), field('repo', 'string')] },
|
|
68
88
|
{ routeId: 'guide', method: 'GET', contentType: 'json', responseType: 'GuidePayload', required: [field('stage', 'string'), field('headline', 'string'), field('steps', 'array'), field('next', 'object'), field('hint', 'object', true)] },
|
|
69
89
|
{ routeId: 'judge', method: 'POST', contentType: 'json', responseType: 'JudgePayload', required: [] },
|
|
70
90
|
{ routeId: 'kernel', method: 'GET', contentType: 'json', responseType: 'KernelNodePayload', required: [field('found', 'boolean'), field('id', 'string'), field('asOf', 'string', true), field('node', 'object', true)] },
|
|
71
|
-
{ routeId: 'value', method: 'GET', contentType: 'json', responseType: 'ValuePayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('gitRepo', 'boolean'), field('valueSource', 'string', true), field('repo', 'string'), field('generatedAt', 'string'), field('realization', 'object', true), field('roi', 'object', true), field('frontier', 'object', true), field('budget', 'object', true), field('allocation', 'object', true), field('projects', 'array'), field('projectAllocation', 'object', true), field('usage', 'object', true), field('selfReported', 'object'), field('team', 'object', true), field('drift', 'object', true), field('reclaimed', 'object', true)] },
|
|
91
|
+
{ routeId: 'value', method: 'GET', contentType: 'json', responseType: 'ValuePayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('gitRepo', 'boolean'), field('valueSource', 'string', true), field('repo', 'string'), field('generatedAt', 'string'), field('realization', 'object', true), field('roi', 'object', true), field('frontier', 'object', true), field('budget', 'object', true), field('allocation', 'object', true), field('projects', 'array', true), field('projectAllocation', 'object', true), field('usage', 'object', true), field('selfReported', 'object'), field('team', 'object', true), field('drift', 'object', true), field('reclaimed', 'object', true)] },
|
|
72
92
|
{ routeId: 'market', method: 'GET', contentType: 'json', responseType: 'MarketPayload', required: [field('status', 'string'), field('categories', 'array'), field('boundary', 'array'), field('cacheErrors', 'array')] },
|
|
73
93
|
{ routeId: 'outcome-record', method: 'POST', contentType: 'json', responseType: 'OutcomeRecordPayload', required: [field('apply', 'boolean'), field('basis', 'string'), field('event', 'object'), field('matchedRequests', 'number'), field('matchedCostUsd', 'number', true), field('linkStatus', 'string')] },
|
|
74
94
|
{ routeId: 'causal', method: 'GET', contentType: 'json', responseType: 'CausalPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('studies', 'array'), field('study', 'object', true), field('causalEvidence', 'string'), field('boundary', 'string')] },
|
|
75
|
-
{ routeId: 'settings', method: 'GET', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
76
|
-
{ routeId: 'settings-update', method: 'POST', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
95
|
+
{ routeId: 'settings', method: 'GET', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('demo', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
96
|
+
{ routeId: 'settings-update', method: 'POST', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('demo', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
77
97
|
{ routeId: 'clear-proposals', method: 'POST', contentType: 'json', responseType: 'ClearProposalsPayload', required: [field('ok', 'boolean'), field('removed', 'number')] },
|
|
78
98
|
];
|
|
79
99
|
export function dashboardPayloadContract(routeId, method) {
|