@coreplane/switchboard 1.241.0 → 1.243.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/dist/assets/config/config.example.yaml +45 -13
- package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
- package/dist/assets/deploy/cloudflare-resident/worker.ts +298 -24
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +5 -5
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +61 -48
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/actor.ts +12 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/budgets.ts +313 -0
- package/dist/assets/src/core/coordinator/contract.ts +3 -1
- package/dist/assets/src/core/coordinator/driver.ts +0 -10
- package/dist/assets/src/core/costs.ts +21 -80
- package/dist/assets/src/core/harness/scope.ts +21 -0
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +48 -5
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +51 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +4 -2
- package/dist/assets/src/core/ship/coordinator.ts +101 -84
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentAutoRebuild.ts +4 -2
- package/dist/assets/src/execution/residentHead.ts +23 -10
- package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
- package/dist/assets/src/execution/residentRefresh.ts +43 -4
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/src/execution/sandboxErrors.ts +6 -0
- package/dist/assets/src/execution/seedPlan.ts +17 -0
- package/dist/assets/web/dist/.vite/manifest.json +443 -446
- package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DACalNVF.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-Dw0T1M-P.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BbLuIAiS.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-twkFmSUY.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
- package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +1 -0
- package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-d-w-tIKt.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-C9m_EB8J.js +9 -0
- package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +4121 -1902
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +0 -2
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// Every wall clock in Switchboard as one table (docs/decisions/0046; docs/reference/specs/harness-pi.md
|
|
2
|
+
// item 15; docs/reference/specs/agent-ship.md item 8). A budget is a LEASE a
|
|
3
|
+
// parent carves from its own remainder, never a constant a file holds on its
|
|
4
|
+
// own: a preset ASKS for a lease and does useful work above a FLOOR; a parent
|
|
5
|
+
// that runs a loop holds back a RESERVE, derived from the floors and the
|
|
6
|
+
// provisioning of every round that must still follow, plus the merge wait's
|
|
7
|
+
// floor; and the lease covers everything the run does, the loop, the write-up
|
|
8
|
+
// and the post-step, each an ALLOWANCE named here. The registry reads its
|
|
9
|
+
// `maxMinutes` and `maxTurns` from this module and `budgets.check.test.ts`
|
|
10
|
+
// asserts the fits, so a number that breaks another's assumption is a red
|
|
11
|
+
// build. `carve` is called by the ship coordinator for every round (agent-ship
|
|
12
|
+
// item 8), `fit` by the config validator at load and by the fork over a clipped
|
|
13
|
+
// request, and `carveChildOfParent` by the conductor's spawn.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately free of node: imports and of the agent registry, so the
|
|
16
|
+
// Workflow-driven coordinator and the deploy Workers can bundle it — the
|
|
17
|
+
// dependency runs registry → budgets, never the reverse.
|
|
18
|
+
|
|
19
|
+
export const MINUTE_MS = 60_000;
|
|
20
|
+
|
|
21
|
+
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
22
|
+
export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
|
|
23
|
+
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
24
|
+
export type Preset = LoopPreset | "ship";
|
|
25
|
+
|
|
26
|
+
/** What each preset asks for when nothing above it is tighter, in minutes. The
|
|
27
|
+
* pipeline's (`ship`) is the wall clock one segment of its loop runs under;
|
|
28
|
+
* a deployment's `ship.maxMinutes` replaces it, held to `fit` below. */
|
|
29
|
+
export const ASKS: Readonly<Record<Preset, number>> = {
|
|
30
|
+
general: 5,
|
|
31
|
+
coding: 45,
|
|
32
|
+
review: 25,
|
|
33
|
+
ship: 120,
|
|
34
|
+
research: 8,
|
|
35
|
+
explore: 120,
|
|
36
|
+
conductor: 120,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The rounds a ship loop is made of. `fix` is a coding child handed the
|
|
40
|
+
* review's findings; `merge` is the runner's wait on the guards. */
|
|
41
|
+
export type RoundKind = "coding" | "review" | "fix" | "merge";
|
|
42
|
+
|
|
43
|
+
/** The least lease in which a round does useful work, in minutes. A carve that
|
|
44
|
+
* falls under the floor is refused rather than dispatched: a two-minute
|
|
45
|
+
* review or fix costs an attach and a model turn and finishes nothing.
|
|
46
|
+
* Review's is the ledger's 90th percentile of completed reviews (5.1 min over
|
|
47
|
+
* 181); coding's and the merge wait's are guesses until the ledger says. */
|
|
48
|
+
export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
49
|
+
general: 2,
|
|
50
|
+
coding: 10,
|
|
51
|
+
review: 5,
|
|
52
|
+
research: 3,
|
|
53
|
+
explore: 15,
|
|
54
|
+
conductor: 15,
|
|
55
|
+
};
|
|
56
|
+
export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
57
|
+
coding: PRESET_FLOORS.coding,
|
|
58
|
+
fix: PRESET_FLOORS.coding,
|
|
59
|
+
review: PRESET_FLOORS.review,
|
|
60
|
+
merge: 10,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The merge wait's own ask: how long the runner waits on the guards at most
|
|
64
|
+
* when the remainder allows it. */
|
|
65
|
+
export const MERGE_WAIT_ASK_MINUTES = 60;
|
|
66
|
+
|
|
67
|
+
/** The ship runner's waits, in minutes: the margin a child's wait allows past
|
|
68
|
+
* its budget, the slice a wait is asked in, the merge door's re-ask cadence,
|
|
69
|
+
* and the pause before a busy spawn is asked again. */
|
|
70
|
+
export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
|
|
71
|
+
|
|
72
|
+
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
73
|
+
* every run or round pays: `provision` is attach and restore before the
|
|
74
|
+
* harness's clock starts; `writeUp` is the final answer after the loop ends;
|
|
75
|
+
* `commandWriteUp` is what the last command leaves for that answer; `execCall`
|
|
76
|
+
* is the exec client's wait past a command's own budget; `bearerGrace` is how
|
|
77
|
+
* far past the lease the model bearer stays valid for the last call's tail. */
|
|
78
|
+
export const ALLOWANCES = {
|
|
79
|
+
provision: 3,
|
|
80
|
+
writeUp: 3,
|
|
81
|
+
commandWriteUp: 1,
|
|
82
|
+
execCall: 0.5,
|
|
83
|
+
bearerGrace: 1,
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
/** The post-step turn a preset runs after its loop, in minutes: the coding
|
|
87
|
+
* run's description turn, the review's verdict turn, none for the rest. */
|
|
88
|
+
export const POST_STEP_MINUTES: Readonly<Record<Preset, number>> = {
|
|
89
|
+
coding: 5,
|
|
90
|
+
review: 3,
|
|
91
|
+
general: 0,
|
|
92
|
+
research: 0,
|
|
93
|
+
explore: 0,
|
|
94
|
+
conductor: 0,
|
|
95
|
+
ship: 0,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** The post-step allowance of a preset named by its def's `name`: a name
|
|
99
|
+
* outside the table (a test's, a command run's) runs no post-step. */
|
|
100
|
+
export function postStepMinutes(preset: string): number {
|
|
101
|
+
return (POST_STEP_MINUTES as Readonly<Record<string, number>>)[preset] ?? 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The post-step turn's lease, in minutes (fractional when the remainder is:
|
|
105
|
+
* the harness turns it back into ms): the preset's allowance, or the lease's
|
|
106
|
+
* remainder when that is less — never under a minute, so a turn the loop's
|
|
107
|
+
* write-up crowded still gets its one chance. The bearer's grace covers that
|
|
108
|
+
* floor for a turn that starts at or before the lease's end; a turn that
|
|
109
|
+
* starts later runs on a bearer that may expire under it, and its refused
|
|
110
|
+
* call fails soft — the run's answer already stands. Without a remainder
|
|
111
|
+
* (no harness session says) the allowance stands. */
|
|
112
|
+
export function postStepLease(preset: string, remainingMs: number | undefined): number {
|
|
113
|
+
const allowance = postStepMinutes(preset);
|
|
114
|
+
if (remainingMs === undefined) return allowance;
|
|
115
|
+
return Math.min(allowance, Math.max(1, remainingMs / MINUTE_MS));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A follow-up turn's lease, in ms: the lesser of what it asks and what the
|
|
119
|
+
* run's lease still holds, never under a minute — the grace covers that floor
|
|
120
|
+
* when the turn starts by the lease's end; later, the bearer may expire under
|
|
121
|
+
* the turn and its call is refused, failing soft. */
|
|
122
|
+
export function turnLeaseMs(askMinutes: number, remainingMs: number): number {
|
|
123
|
+
return Math.min(askMinutes * MINUTE_MS, Math.max(MINUTE_MS, remainingMs));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The wrap-up warning's place before the loop's end: three minutes, or a
|
|
127
|
+
* quarter of the loop when the loop is shorter than twelve. */
|
|
128
|
+
export const WRAP_UP_WARNING = { minutes: 3, fraction: 0.25 } as const;
|
|
129
|
+
|
|
130
|
+
/** The clocks a harness keeps for one lease (docs/reference/specs/harness-pi.md
|
|
131
|
+
* items 6 and 15). The lease ENDS at `deadline`; the LOOP ENDS at `loopEnd`,
|
|
132
|
+
* the write-up allowance and the preset's post-step earlier, so both run
|
|
133
|
+
* inside the lease; the WARNING is steered at `warnAt`; the write-up is
|
|
134
|
+
* BOUNDED by `finaleMs`. A follow-up turn on the session (`kind: "turn"`)
|
|
135
|
+
* holds nothing back — its deliverable is a tool call, not a write-up — and
|
|
136
|
+
* its loop ends at its deadline. A lease shorter than its hold-back has no
|
|
137
|
+
* loop time: the loop ends at its start, never before it. */
|
|
138
|
+
export interface LoopClock {
|
|
139
|
+
startedAt: number;
|
|
140
|
+
deadline: number;
|
|
141
|
+
loopEnd: number;
|
|
142
|
+
warnAt: number;
|
|
143
|
+
finaleMs: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function loopClock(
|
|
147
|
+
startedAt: number,
|
|
148
|
+
remainingMs: number,
|
|
149
|
+
preset: string,
|
|
150
|
+
kind: "loop" | "turn" = "loop",
|
|
151
|
+
): LoopClock {
|
|
152
|
+
const deadline = startedAt + remainingMs;
|
|
153
|
+
const holdBackMs = kind === "loop" ? (ALLOWANCES.writeUp + postStepMinutes(preset)) * MINUTE_MS : 0;
|
|
154
|
+
const loopEnd = Math.max(startedAt, deadline - holdBackMs);
|
|
155
|
+
const warnAt =
|
|
156
|
+
loopEnd - Math.min(WRAP_UP_WARNING.minutes * MINUTE_MS, (loopEnd - startedAt) * WRAP_UP_WARNING.fraction);
|
|
157
|
+
return { startedAt, deadline, loopEnd, warnAt, finaleMs: ALLOWANCES.writeUp * MINUTE_MS };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** When a run's model-proxy bearer expires: the lease's end plus the grace
|
|
161
|
+
* (docs/reference/specs/model-proxy.md item 2). */
|
|
162
|
+
export function bearerExpiresAt(leaseEndsAt: number): number {
|
|
163
|
+
return leaseEndsAt + ALLOWANCES.bearerGrace * MINUTE_MS;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The bearer's expiry as the mint at provisioning sets it, before the lease
|
|
167
|
+
* has started: the provisioning allowance, the lease and the grace — replaced
|
|
168
|
+
* by `bearerExpiresAt` the moment the harness starts the lease. */
|
|
169
|
+
export function provisionalBearerExpiresAt(now: number, leaseMinutes: number): number {
|
|
170
|
+
return now + (ALLOWANCES.provision + leaseMinutes + ALLOWANCES.bearerGrace) * MINUTE_MS;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The per-command bash budget's rows: the default when a call names none, the
|
|
174
|
+
* ceiling a call may raise it to, and the floor under which a number is a
|
|
175
|
+
* typo (docs/reference/specs/execution.md item 11). */
|
|
176
|
+
export const BASH_COMMAND = { defaultMinutes: 5, maxMinutes: 20, minMs: 1_000 } as const;
|
|
177
|
+
|
|
178
|
+
/** The pace that marks a run as looping rather than working: a model turn
|
|
179
|
+
* every ten seconds, sustained for the whole wall clock. A busy run takes
|
|
180
|
+
* 20–40 s a turn (a model think plus a tool call), so a run that averages six
|
|
181
|
+
* a minute from start to end is re-issuing calls, not making progress — and
|
|
182
|
+
* its turn cap ends it before the wall clock would, with a write-up that
|
|
183
|
+
* says so (docs/reference/specs/harness-pi.md item 15). */
|
|
184
|
+
export const RUNAWAY_TURNS_PER_MINUTE = 6;
|
|
185
|
+
|
|
186
|
+
/** The turn cap a wall clock implies: `maxMinutes × RUNAWAY_TURNS_PER_MINUTE`.
|
|
187
|
+
* Every preset that runs the loop derives its `maxTurns` from this, so the
|
|
188
|
+
* cap is never a number a good run reaches — the minutes are the budget. */
|
|
189
|
+
export function runawayTurnCap(maxMinutes: number): number {
|
|
190
|
+
return maxMinutes * RUNAWAY_TURNS_PER_MINUTE;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A pipeline's loop as the config allows it: `maxRounds` counts review rounds. */
|
|
194
|
+
export interface Loop {
|
|
195
|
+
maxRounds: number;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A round's position in its loop: its kind and its index, so the reserve is
|
|
199
|
+
* the rounds after that index, never a table keyed by kind alone. */
|
|
200
|
+
export interface RoundPosition {
|
|
201
|
+
kind: RoundKind;
|
|
202
|
+
index: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The rounds of a loop in order: the coding round, then a review and, after
|
|
206
|
+
* every review but the last, a fix, then the merge wait. */
|
|
207
|
+
export function loopRounds(loop: Loop): RoundKind[] {
|
|
208
|
+
const rounds: RoundKind[] = ["coding"];
|
|
209
|
+
for (let i = 0; i < loop.maxRounds; i++) {
|
|
210
|
+
rounds.push("review");
|
|
211
|
+
if (i < loop.maxRounds - 1) rounds.push("fix");
|
|
212
|
+
}
|
|
213
|
+
rounds.push("merge");
|
|
214
|
+
return rounds;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Where a round sits in `loopRounds`: the coding round first; review round
|
|
218
|
+
* `n` (counted from 1) and the fix that follows it at `2n − 1` and `2n`; the
|
|
219
|
+
* merge wait last. The ship coordinator numbers its rounds this way, so the
|
|
220
|
+
* reserve it carves with is the one the fit assumed. */
|
|
221
|
+
export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
|
|
222
|
+
switch (kind) {
|
|
223
|
+
case "coding":
|
|
224
|
+
return 0;
|
|
225
|
+
case "review":
|
|
226
|
+
return 2 * n - 1;
|
|
227
|
+
case "fix":
|
|
228
|
+
return 2 * n;
|
|
229
|
+
case "merge":
|
|
230
|
+
return 2 * loop.maxRounds;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A child spawned outside any loop (a conductor's): the parent's whole
|
|
235
|
+
* remainder in whole minutes, refused under the child preset's floor — a
|
|
236
|
+
* child under its floor costs a thread and a model turn and finishes nothing. */
|
|
237
|
+
export function carveChildOfParent(
|
|
238
|
+
remainingMs: number,
|
|
239
|
+
preset: LoopPreset,
|
|
240
|
+
): { kind: "carved"; minutes: number } | { kind: "refused"; reason: "under floor"; minutes: number; floor: number } {
|
|
241
|
+
const minutes = Math.max(0, Math.floor(remainingMs / MINUTE_MS));
|
|
242
|
+
const floor = PRESET_FLOORS[preset];
|
|
243
|
+
if (minutes < floor) return { kind: "refused", reason: "under floor", minutes, floor };
|
|
244
|
+
return { kind: "carved", minutes };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The minutes a round holds back for what must follow it in the loop: the
|
|
248
|
+
* floor plus provisioning of every later review and fix, and the merge
|
|
249
|
+
* wait's floor. A merge holds nothing back; a round with no loop (a
|
|
250
|
+
* conductor's child) holds nothing back either. */
|
|
251
|
+
export function reserveMinutes(
|
|
252
|
+
round: RoundPosition,
|
|
253
|
+
loop: Loop | undefined,
|
|
254
|
+
floors: Readonly<Record<RoundKind, number>> = FLOORS,
|
|
255
|
+
): number {
|
|
256
|
+
if (!loop) return 0;
|
|
257
|
+
let reserve = 0;
|
|
258
|
+
for (const kind of loopRounds(loop).slice(round.index + 1)) {
|
|
259
|
+
reserve += kind === "merge" ? floors.merge : floors[kind] + ALLOWANCES.provision;
|
|
260
|
+
}
|
|
261
|
+
return reserve;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** What a round asks for: the preset's ask for a coding or fix round, the
|
|
265
|
+
* review's for a review, the merge wait's own for the merge. */
|
|
266
|
+
export function roundAskMinutes(kind: RoundKind): number {
|
|
267
|
+
switch (kind) {
|
|
268
|
+
case "coding":
|
|
269
|
+
case "fix":
|
|
270
|
+
return ASKS.coding;
|
|
271
|
+
case "review":
|
|
272
|
+
return ASKS.review;
|
|
273
|
+
case "merge":
|
|
274
|
+
return MERGE_WAIT_ASK_MINUTES;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A carve's result: the minutes with what bounded them (`ask` when the round
|
|
279
|
+
* got its whole ask, `parent` when the remainder minus the reserve was
|
|
280
|
+
* tighter) and what the parent holds back, or a refusal when the minutes the
|
|
281
|
+
* remainder leaves fall under the round's floor. */
|
|
282
|
+
export type Carve =
|
|
283
|
+
| { kind: "carved"; minutes: number; boundedBy: "ask" | "parent"; holds: number }
|
|
284
|
+
| { kind: "refused"; reason: "under floor"; minutes: number; floor: number; holds: number };
|
|
285
|
+
|
|
286
|
+
/** The only place a round's minutes are computed: the parent's remainder in
|
|
287
|
+
* whole minutes minus the round's reserve, capped at the round's ask, refused
|
|
288
|
+
* under the round's floor. `loop` is undefined for a child outside any loop. */
|
|
289
|
+
export function carve(remainingMs: number, round: RoundPosition, loop: Loop | undefined): Carve {
|
|
290
|
+
const holds = reserveMinutes(round, loop);
|
|
291
|
+
const ask = roundAskMinutes(round.kind);
|
|
292
|
+
const headroom = Math.floor(remainingMs / MINUTE_MS) - holds;
|
|
293
|
+
const minutes = Math.min(ask, headroom);
|
|
294
|
+
const floor = FLOORS[round.kind];
|
|
295
|
+
// A refusal reports what the remainder left, never a negative number.
|
|
296
|
+
if (minutes < floor) return { kind: "refused", reason: "under floor", minutes: Math.max(0, minutes), floor, holds };
|
|
297
|
+
return { kind: "carved", minutes, boundedBy: minutes === ask ? "ask" : "parent", holds };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** A pipeline's wall clock and its loop: what `fit` judges. */
|
|
301
|
+
export interface Pipeline extends Loop {
|
|
302
|
+
maxMinutes: number;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The fit: a pipeline holds its first child at its ask and every later round
|
|
306
|
+
* at its floor — `provision + ask(coding) + reserve(coding, loop) ≤
|
|
307
|
+
* maxMinutes`. Asserted at verify over the registry, at config load over the
|
|
308
|
+
* deployment's `ship` block (`validateShip`), and at the fork over a request a
|
|
309
|
+
* boundary or a `budget:` directive clipped. `need` is the sum a refusal names. */
|
|
310
|
+
export function fit(pipeline: Pipeline): { ok: boolean; need: number; have: number } {
|
|
311
|
+
const need = ALLOWANCES.provision + ASKS.coding + reserveMinutes({ kind: "coding", index: 0 }, pipeline);
|
|
312
|
+
return { ok: pipeline.maxMinutes >= need, need, have: pipeline.maxMinutes };
|
|
313
|
+
}
|
|
@@ -150,6 +150,8 @@ export interface CoordinatorInstance {
|
|
|
150
150
|
/** The bound credential behind the person (authorization.md item 15), when
|
|
151
151
|
* there was one: every child is authorized under ITS grants, as the request was. */
|
|
152
152
|
authenticatedAs?: string;
|
|
153
|
+
/** The app that relayed the request for the person (authorization.md item 14), when one did: every child is authorized under app ∩ person, as the request was. */
|
|
154
|
+
postedBy?: string;
|
|
153
155
|
channelId: string;
|
|
154
156
|
channelName?: string;
|
|
155
157
|
/** The requesting thread: where the card lives and where a generated plan's
|
|
@@ -260,7 +262,7 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
260
262
|
if (r.kind !== "ship") return false;
|
|
261
263
|
if (!isText(r.userId) || !isText(r.channelId) || !isText(r.threadKey)) return false;
|
|
262
264
|
if (!isOptionalText(r.userName) || !isOptionalText(r.channelName) || !isOptionalText(r.sourceUrl)) return false;
|
|
263
|
-
if (!isOptionalText(r.authenticatedAs)) return false;
|
|
265
|
+
if (!isOptionalText(r.authenticatedAs) || !isOptionalText(r.postedBy)) return false;
|
|
264
266
|
if (typeof r.repo !== "string" || !REPO_SLUG.test(r.repo)) return false;
|
|
265
267
|
if (!isText(r.branch) || !isOptionalText(r.base)) return false;
|
|
266
268
|
if (!isFinite(r.createdAt)) return false;
|
|
@@ -54,7 +54,6 @@ import {
|
|
|
54
54
|
type ShipCaps,
|
|
55
55
|
type StepReturn,
|
|
56
56
|
type UnitEnding,
|
|
57
|
-
type UnitPipelineInput,
|
|
58
57
|
type UnitPipelineState,
|
|
59
58
|
} from "../ship/coordinator.js";
|
|
60
59
|
import { checksSettledEventType, isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
@@ -170,7 +169,6 @@ interface PlanFacts {
|
|
|
170
169
|
repo: string;
|
|
171
170
|
base: string;
|
|
172
171
|
caps: ShipCaps;
|
|
173
|
-
childMinutes: UnitPipelineInput["childMinutes"];
|
|
174
172
|
units: CoordinatorUnit[];
|
|
175
173
|
}
|
|
176
174
|
|
|
@@ -184,12 +182,6 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
184
182
|
if (typeof b.repo !== "string" || typeof b.base !== "string") throw new UnreadableAnswer("plan", a, "repo and base");
|
|
185
183
|
if (!isMinutes(b.caps) || typeof b.caps.maxRounds !== "number" || typeof b.caps.maxMinutes !== "number")
|
|
186
184
|
throw new UnreadableAnswer("plan", a, "caps");
|
|
187
|
-
if (
|
|
188
|
-
!isMinutes(b.childMinutes) ||
|
|
189
|
-
typeof b.childMinutes.coding !== "number" ||
|
|
190
|
-
typeof b.childMinutes.review !== "number"
|
|
191
|
-
)
|
|
192
|
-
throw new UnreadableAnswer("plan", a, "childMinutes");
|
|
193
185
|
const units: unknown = b.units;
|
|
194
186
|
if (!Array.isArray(units) || !units.every(isCoordinatorUnit)) throw new UnreadableAnswer("plan", a, "units");
|
|
195
187
|
return {
|
|
@@ -204,7 +196,6 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
204
196
|
repo: b.repo,
|
|
205
197
|
base: b.base,
|
|
206
198
|
caps: { maxRounds: b.caps.maxRounds, maxMinutes: b.caps.maxMinutes },
|
|
207
|
-
childMinutes: { coding: b.childMinutes.coding, review: b.childMinutes.review },
|
|
208
199
|
units,
|
|
209
200
|
};
|
|
210
201
|
}
|
|
@@ -486,7 +477,6 @@ async function runUnit(
|
|
|
486
477
|
repo: plan.repo,
|
|
487
478
|
base: plan.base,
|
|
488
479
|
caps: plan.caps,
|
|
489
|
-
childMinutes: plan.childMinutes,
|
|
490
480
|
// The instance's field decides who merges (record 0031's merge grant),
|
|
491
481
|
// carried here by the plan route: the hand-off wrote `runner` on a
|
|
492
482
|
// seeded plan and `person` on a task, and the door re-checks it — the
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
anthropicTokensCostUsd,
|
|
3
|
+
parseModelPrices,
|
|
4
|
+
type AnthropicTokens,
|
|
5
|
+
type ModelPriceTable,
|
|
6
|
+
} from "./modelPricing.js";
|
|
1
7
|
import { systemClock } from "./trace/clock.js";
|
|
2
8
|
// Spend report: what a group of deployed pieces ("switchboard" = the bot
|
|
3
9
|
// Worker + its containers, the resident/sandbox/memory Workers) costs per day,
|
|
@@ -68,10 +74,15 @@ export interface CostsConfig {
|
|
|
68
74
|
/** Env var holding an Anthropic Admin API key (sk-ant-admin…). Optional feature. */
|
|
69
75
|
anthropicAdminKeyEnv: string;
|
|
70
76
|
groups: Record<string, CostGroupConfig>;
|
|
77
|
+
/** `costs.prices`: the operator's per-million rates by `<provider>/<model>`, over the Anthropic list (item 4b). */
|
|
78
|
+
prices: ModelPriceTable;
|
|
71
79
|
/** The snapshot the page and the twins serve (src/core/costsSnapshot.ts). */
|
|
72
80
|
snapshot: {
|
|
73
81
|
/** Hours between two reads of the billing sources. */
|
|
74
82
|
everyHours: number;
|
|
83
|
+
/** The platform-namespaced channel (`slack:C…`) told when takes keep failing and when they
|
|
84
|
+
* land again (`ALERT_AFTER_FAILURES` in a row); absent → the status alone says so. */
|
|
85
|
+
alertChannel?: string;
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -82,13 +93,15 @@ const DEFAULT_ANTHROPIC_ADMIN_ENV = "ANTHROPIC_ADMIN_KEY";
|
|
|
82
93
|
* default — both sources bucket by UTC day, and the page is read about as often. */
|
|
83
94
|
export const COSTS_SNAPSHOT_EVERY_HOURS = Object.freeze({ default: 24, min: 1, max: 168 });
|
|
84
95
|
|
|
85
|
-
/** `costs.snapshot`: absent → the default interval; a value outside the
|
|
96
|
+
/** `costs.snapshot`: absent → the default interval and no alert channel; a value outside the
|
|
97
|
+
* bounds or not a whole number throws by name; `alertChannel`, when given, is a platform-namespaced id. */
|
|
86
98
|
function snapshotConfig(raw: unknown): CostsConfig["snapshot"] {
|
|
87
99
|
if (raw === undefined) return { everyHours: COSTS_SNAPSHOT_EVERY_HOURS.default };
|
|
88
100
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
89
101
|
throw new Error("costs.snapshot must be a mapping");
|
|
90
|
-
const
|
|
91
|
-
|
|
102
|
+
const r = raw as Record<string, unknown>;
|
|
103
|
+
// `undefined` alone defaults: a bare `everyHours:` key (null) is a malformed value, refused below by name.
|
|
104
|
+
const every = r.everyHours === undefined ? COSTS_SNAPSHOT_EVERY_HOURS.default : r.everyHours;
|
|
92
105
|
if (
|
|
93
106
|
typeof every !== "number" ||
|
|
94
107
|
!Number.isInteger(every) ||
|
|
@@ -98,7 +111,10 @@ function snapshotConfig(raw: unknown): CostsConfig["snapshot"] {
|
|
|
98
111
|
throw new Error(
|
|
99
112
|
`costs.snapshot.everyHours must be a whole number of hours between ${COSTS_SNAPSHOT_EVERY_HOURS.min} and ${COSTS_SNAPSHOT_EVERY_HOURS.max}`,
|
|
100
113
|
);
|
|
101
|
-
return { everyHours: every };
|
|
114
|
+
if (r.alertChannel === undefined) return { everyHours: every };
|
|
115
|
+
if (typeof r.alertChannel !== "string" || !/^[a-z]+:.+$/.test(r.alertChannel))
|
|
116
|
+
throw new Error("costs.snapshot.alertChannel must be a platform-namespaced channel id (`slack:C…`)");
|
|
117
|
+
return { everyHours: every, alertChannel: r.alertChannel };
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
function labelMap(raw: unknown, what: string): Record<string, string> {
|
|
@@ -147,6 +163,7 @@ export function parseCostsConfig(raw: unknown): CostsConfig | undefined {
|
|
|
147
163
|
anthropicAdminKeyEnv:
|
|
148
164
|
typeof r.anthropicAdminKeyEnv === "string" ? r.anthropicAdminKeyEnv : DEFAULT_ANTHROPIC_ADMIN_ENV,
|
|
149
165
|
groups,
|
|
166
|
+
prices: parseModelPrices(r.prices),
|
|
150
167
|
snapshot: snapshotConfig(r.snapshot),
|
|
151
168
|
};
|
|
152
169
|
}
|
|
@@ -683,82 +700,6 @@ export function buildCostReport(
|
|
|
683
700
|
};
|
|
684
701
|
}
|
|
685
702
|
|
|
686
|
-
// ---- Anthropic list prices ----------------------------------------------------------------
|
|
687
|
-
|
|
688
|
-
/** USD per million tokens of one kind, one model family. */
|
|
689
|
-
export interface AnthropicModelPrice {
|
|
690
|
-
input: number;
|
|
691
|
-
output: number;
|
|
692
|
-
cacheWrite5m: number;
|
|
693
|
-
cacheWrite1h: number;
|
|
694
|
-
cacheRead: number;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/** Anthropic list prices per model family (platform.claude.com/docs/en/about-claude/pricing;
|
|
698
|
-
* re-check when it moves). Keyed by the family id the usage report spells — a dated
|
|
699
|
-
* release (`claude-haiku-4-5-20251001`) resolves to its family through
|
|
700
|
-
* `anthropicPriceOf`. Only what the estimate needs: the open day priced at
|
|
701
|
-
* the rate it will bill at; the cost report remains the invoice. */
|
|
702
|
-
export const ANTHROPIC_PRICES: Record<string, AnthropicModelPrice> = {
|
|
703
|
-
"claude-fable-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
|
|
704
|
-
"claude-mythos-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
|
|
705
|
-
"claude-fable-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
|
|
706
|
-
"claude-mythos-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
|
|
707
|
-
"claude-opus-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
708
|
-
"claude-opus-4-8": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
709
|
-
"claude-opus-4-7": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
710
|
-
"claude-opus-4-6": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
711
|
-
"claude-opus-4-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
712
|
-
"claude-opus-4-1": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
713
|
-
"claude-opus-4": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
714
|
-
"claude-sonnet-5": { input: 2, output: 10, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.2 },
|
|
715
|
-
"claude-sonnet-4-6": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
|
|
716
|
-
"claude-sonnet-4-5": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
|
|
717
|
-
"claude-sonnet-4": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
|
|
718
|
-
"claude-haiku-4-5": { input: 1, output: 5, cacheWrite5m: 1.25, cacheWrite1h: 2, cacheRead: 0.1 },
|
|
719
|
-
"claude-haiku-3-5": { input: 0.8, output: 4, cacheWrite5m: 1, cacheWrite1h: 1.6, cacheRead: 0.08 },
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
const DATED_RELEASE_SUFFIX = /^-\d{8}$/;
|
|
723
|
-
|
|
724
|
-
/** The family prices of a model id: the id itself, or the id less a dated
|
|
725
|
-
* release suffix (`-YYYYMMDD`). Nothing else counts as "the same family" —
|
|
726
|
-
* `claude-fable-5-1` is not `claude-fable-5` with a suffix, and its cache
|
|
727
|
-
* reads bill differently. Unknown → undefined, never a guess. */
|
|
728
|
-
export function anthropicPriceOf(modelId: string): AnthropicModelPrice | undefined {
|
|
729
|
-
const exact = ANTHROPIC_PRICES[modelId];
|
|
730
|
-
if (exact) return exact;
|
|
731
|
-
for (const family of Object.keys(ANTHROPIC_PRICES)) {
|
|
732
|
-
if (modelId.startsWith(family) && DATED_RELEASE_SUFFIX.test(modelId.slice(family.length)))
|
|
733
|
-
return ANTHROPIC_PRICES[family];
|
|
734
|
-
}
|
|
735
|
-
return undefined;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/** Token counts of one usage-report row, in the report's own kinds. */
|
|
739
|
-
export interface AnthropicTokens {
|
|
740
|
-
uncachedInput: number;
|
|
741
|
-
output: number;
|
|
742
|
-
cacheRead: number;
|
|
743
|
-
cacheWrite5m: number;
|
|
744
|
-
cacheWrite1h: number;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/** What those tokens cost at the family's list prices; undefined for a model
|
|
748
|
-
* the table does not know (the caller reports the tokens, never $0). */
|
|
749
|
-
export function anthropicTokensCostUsd(modelId: string, t: AnthropicTokens): number | undefined {
|
|
750
|
-
const p = anthropicPriceOf(modelId);
|
|
751
|
-
if (!p) return undefined;
|
|
752
|
-
return (
|
|
753
|
-
(t.uncachedInput * p.input +
|
|
754
|
-
t.output * p.output +
|
|
755
|
-
t.cacheRead * p.cacheRead +
|
|
756
|
-
t.cacheWrite5m * p.cacheWrite5m +
|
|
757
|
-
t.cacheWrite1h * p.cacheWrite1h) /
|
|
758
|
-
1_000_000
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
703
|
// ---- range ----------------------------------------------------------------------------
|
|
763
704
|
|
|
764
705
|
const DEFAULT_DAYS = 30;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Where a preset's harness word is set (docs/reference/specs/harness.md item
|
|
2
|
+
// 8), in a module with no imports of its own: the run record (`run_meta.harnessScope`)
|
|
3
|
+
// and the timeline fold read it, and both are compiled into the Workers' and
|
|
4
|
+
// the dashboard's programs, where the roster's neighbours — the harness
|
|
5
|
+
// objects, their Node-only process code — must not follow. The roster
|
|
6
|
+
// re-exports these names, so every reader inside the bot can still take them
|
|
7
|
+
// from there.
|
|
8
|
+
|
|
9
|
+
/** The scopes a word is set at, most specific first — the order the resolution
|
|
10
|
+
* walks the layers: the requester's own scope, the channel's, then the
|
|
11
|
+
* deployment's top-level `harness` block, which is the defaults layer under
|
|
12
|
+
* its one spelling. What `run_meta.harnessScope` and the config block name, so
|
|
13
|
+
* a reader of a run on OpenCode never guesses whose word put it there. */
|
|
14
|
+
export const HARNESS_SCOPES = ["user", "channel", "defaults"] as const;
|
|
15
|
+
export type HarnessScope = (typeof HARNESS_SCOPES)[number];
|
|
16
|
+
|
|
17
|
+
/** Whether a value is one of the scopes a word is set at: the timeline's test
|
|
18
|
+
* for the field a record carries. */
|
|
19
|
+
export function isHarnessScope(value: unknown): value is HarnessScope {
|
|
20
|
+
return typeof value === "string" && (HARNESS_SCOPES as readonly string[]).includes(value);
|
|
21
|
+
}
|