@mindot/will 0.7.0 → 0.8.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/README.md +87 -22
- package/dist/channels/discord.d.ts +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/cli.js +10823 -10454
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +562 -226
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-DAW0l-lY.d.ts → will-cS6k4uiJ.d.ts} +470 -84
- package/package.json +1 -1
- package/src/cognition/agency/engines/action.selector.ts +2 -1
- package/src/cognition/agency/engines/reafference.engine.ts +12 -2
- package/src/cognition/agency/reconcile.learning.ts +16 -2
- package/src/cognition/agency/schemas/repertoire.ts +12 -5
- package/src/cognition/config.mirror.entities.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +136 -58
- package/src/cognition/faculties/executive.engine/facet.ts +10 -2
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +2 -1
- package/src/cognition/index.ts +4 -0
- package/src/cognition/memory/vector.embedder.ts +9 -5
- package/src/cognition/utilities/token.tracker.ts +191 -96
- package/src/host/boot.ts +78 -22
- package/src/index.ts +35 -0
- package/src/llm/index.ts +397 -96
- package/src/llm/routing.ts +198 -0
- package/src/llm/summarizer.ts +5 -1
- package/src/runners/thin-shim.runner.ts +18 -6
- package/src/sdk/will.ts +82 -16
- package/src/stem/guards/identity.coherence.ts +17 -6
- package/src/stem/index.ts +3 -3
- package/src/stem/mind.ts +155 -24
- package/src/stem/policy/arbiter.ts +49 -14
- package/src/stem/policy/rule.table.ts +2 -2
- package/src/stem/tracts/effector.controller.ts +56 -9
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* Exposes as metrics so the orchestrator and runner can log costs,
|
|
17
17
|
* and the ParameterOptimizer can factor cost into optimization decisions.
|
|
18
18
|
*/
|
|
19
|
+
import { logger } from '#core/logger'
|
|
19
20
|
import type {
|
|
20
21
|
Duration,
|
|
21
22
|
Tick,
|
|
@@ -31,56 +32,46 @@ import { wallClock } from '#core/wall.clock'
|
|
|
31
32
|
// transport layer (determinism contract). The tracker exposes a neutral
|
|
32
33
|
// onRecord() sink; the stem bridges records onto the transport.
|
|
33
34
|
|
|
35
|
+
// ── Attribution axes ──────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// Typed rather than free strings so a deviation is caught at the call site
|
|
38
|
+
// instead of surfacing as a silently-unmatched routing rule or a cost bucket
|
|
39
|
+
// nobody notices is empty. These live here (not in #llm) because cognition is
|
|
40
|
+
// the lower layer — #llm already imports from this module, and the reverse
|
|
41
|
+
// would be circular.
|
|
42
|
+
|
|
43
|
+
/** Top-level cost bucket for an LLM call. */
|
|
44
|
+
export type LLMCallCategory =
|
|
45
|
+
| 'executive' // the master consciousness and its facets
|
|
46
|
+
| 'summarizer' // rolling memory consolidation
|
|
47
|
+
| 'embedding' // semantic-memory vectorisation
|
|
48
|
+
| 'identity-guard' // creation-time persona review
|
|
49
|
+
|
|
50
|
+
/** The actor/subsystem doing the work. */
|
|
51
|
+
export type LLMCallAttribute =
|
|
52
|
+
| 'master' // the executive itself
|
|
53
|
+
| 'facet' // a spawned focus (conversation, planning, outreach, supervision)
|
|
54
|
+
| 'memory' // consolidation / embedding
|
|
55
|
+
| 'guard' // a safety reviewer
|
|
56
|
+
|
|
57
|
+
/** The specific cognitive function being paid for. */
|
|
58
|
+
export type LLMCallFunction =
|
|
59
|
+
| 'decision' // the master's fused decision call
|
|
60
|
+
| 'ideation' // the deliberate path's propose pass
|
|
61
|
+
| 'deliberation' // action choice under contest
|
|
62
|
+
| 'conversation' // a live reply
|
|
63
|
+
| 'outreach' // an unprompted message
|
|
64
|
+
| 'planning' // plan formation / revision
|
|
65
|
+
| 'supervision' // plan-step supervision
|
|
66
|
+
| 'consolidation' // rolling summary
|
|
67
|
+
| 'recall' // embedding a query
|
|
68
|
+
| 'index' // embedding a write
|
|
69
|
+
| 'identity-coherence' // persona review
|
|
70
|
+
|
|
34
71
|
/** One attributed ledger record (5-axis attribution + tokens + cost). */
|
|
35
72
|
export type TokenLedgerRecord = Record<string, unknown>
|
|
36
73
|
export type TokenRecordListener = ( record: TokenLedgerRecord ) => void
|
|
37
74
|
|
|
38
|
-
// ── Pricing table (USD per 1M tokens) ─────────────────────
|
|
39
|
-
|
|
40
|
-
const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
41
|
-
// OpenAI
|
|
42
|
-
'openai/gpt-4o': { input: 2.50, output: 10.00 },
|
|
43
|
-
'openai/gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
44
|
-
'openai/gpt-4-turbo': { input: 10.00, output: 30.00 },
|
|
45
|
-
'openai/gpt-3.5-turbo': { input: 0.50, output: 1.50 },
|
|
46
|
-
|
|
47
|
-
// Anthropic (Claude 4.x family — prices per 1M tokens)
|
|
48
|
-
'anthropic/claude-haiku-4-5': { input: 1.00, output: 5.00 },
|
|
49
|
-
'anthropic/claude-sonnet-4-5': { input: 3.00, output: 15.00 },
|
|
50
|
-
'anthropic/claude-sonnet-4-6': { input: 3.00, output: 15.00 },
|
|
51
|
-
'anthropic/claude-opus-4-7': { input: 5.00, output: 25.00 },
|
|
52
|
-
// Legacy aliases kept for backward compat
|
|
53
|
-
'anthropic/claude-haiku-4': { input: 1.00, output: 5.00 },
|
|
54
|
-
'anthropic/claude-opus-4': { input: 5.00, output: 25.00 },
|
|
55
|
-
|
|
56
|
-
// Z.ai (GLM-5 family). `glm-5.2[1m]` is the same model asking for its 1M
|
|
57
|
-
// context window — same rate, so it gets its own row rather than relying on
|
|
58
|
-
// the normalizer (a future long-context tier would price differently).
|
|
59
|
-
'glm/glm-5.2': { input: 1.40, output: 4.40 },
|
|
60
|
-
'glm/glm-5.2[1m]': { input: 1.40, output: 4.40 },
|
|
61
|
-
|
|
62
|
-
// Google
|
|
63
|
-
'google/gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
64
|
-
'google/gemini-2.0-pro': { input: 1.25, output: 5.00 },
|
|
65
|
-
|
|
66
|
-
// Meta (via Groq/Replicate)
|
|
67
|
-
'meta/llama-3.3-70b': { input: 0.59, output: 0.79 },
|
|
68
|
-
'meta/llama-4-maverick': { input: 0.20, output: 0.60 },
|
|
69
|
-
|
|
70
|
-
// DeepSeek
|
|
71
|
-
'deepseek/deepseek-v3': { input: 0.27, output: 1.10 },
|
|
72
|
-
'deepseek/deepseek-r1': { input: 0.55, output: 2.19 },
|
|
73
|
-
|
|
74
|
-
// Embeddings (input-only — no completion tokens). Priced per 1M input tokens.
|
|
75
|
-
'openai/text-embedding-3-small': { input: 0.02, output: 0 },
|
|
76
|
-
'openai/text-embedding-3-large': { input: 0.13, output: 0 },
|
|
77
|
-
'google/text-embedding-004': { input: 0, output: 0 }, // free tier
|
|
78
|
-
'google/gemini-embedding-001': { input: 0, output: 0 }, // free tier
|
|
79
|
-
|
|
80
|
-
// Fallback for unknown models
|
|
81
|
-
'__default__': { input: 3.00, output: 15.00 },
|
|
82
|
-
}
|
|
83
|
-
|
|
84
75
|
// ── Prompt-cache pricing (Anthropic) ──────────────────────
|
|
85
76
|
// `input_tokens` in the API usage already EXCLUDES cached tokens, so the full
|
|
86
77
|
// input cost is: fresh input ×1 + cache reads ×0.1 + cache writes ×1.25.
|
|
@@ -88,40 +79,83 @@ const CACHE_READ_MULT = 0.1
|
|
|
88
79
|
const CACHE_WRITE_MULT = 1.25
|
|
89
80
|
|
|
90
81
|
/**
|
|
91
|
-
* Normalize a model id to its bare
|
|
92
|
-
* ("claude-sonnet-
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
82
|
+
* Normalize a model id to its bare form so a raw provider string
|
|
83
|
+
* ("claude-sonnet-5-20260114", "anthropic/claude-haiku-4-5", "glm-5.2[1m]")
|
|
84
|
+
* matches a host price keyed plainly — and vice versa. Exact keys are tried
|
|
85
|
+
* first, so a host that prices a long-context variant differently just lists it
|
|
86
|
+
* verbatim and that wins.
|
|
96
87
|
*/
|
|
97
88
|
function normalizeModelKey( model: string ): string {
|
|
98
89
|
let m = model.toLowerCase().trim()
|
|
99
90
|
const slash = m.lastIndexOf('/')
|
|
100
|
-
if( slash >= 0 ) m = m.slice( slash + 1 )
|
|
101
|
-
|
|
91
|
+
if( slash >= 0 ) m = m.slice( slash + 1 ) // drop "provider/" prefix
|
|
92
|
+
m = m.replace( /\[[^\]]*\]$/, '') // drop a trailing qualifier, e.g. "[1m]"
|
|
93
|
+
return m.replace( /[-@]\d{6,8}$/, '') // drop trailing -YYYYMMDD date stamp
|
|
102
94
|
}
|
|
95
|
+
/** USD per 1M tokens for one model. */
|
|
96
|
+
export interface ModelPrice { input: number; output: number }
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Host-supplied prices, keyed by model id. Matching is exact first, then
|
|
100
|
+
* normalized (provider prefix, date stamp and context qualifier stripped), so
|
|
101
|
+
* `claude-sonnet-5` matches `claude-sonnet-5-20260114`.
|
|
102
|
+
*
|
|
103
|
+
* Prices belong to the host: they change on a vendor's schedule, differ per
|
|
104
|
+
* account, and are ~0 for a self-hosted model. The engine ships none.
|
|
105
|
+
*/
|
|
106
|
+
export type PriceTable = Record<string, ModelPrice>
|
|
107
|
+
|
|
108
|
+
/** Models already warned about as unpriced — one line each, not one per call. */
|
|
109
|
+
const _unpricedWarned = new Set<string>()
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the price for a model id from the host's table.
|
|
113
|
+
*
|
|
114
|
+
* The engine ships no prices at all. A table baked into a release is wrong the
|
|
115
|
+
* week a vendor changes a rate, differs per account, and is meaningless for a
|
|
116
|
+
* self-hosted model — and a *partial* table is worse than none, because some
|
|
117
|
+
* models then report plausible-but-stale numbers while others honestly report
|
|
118
|
+
* nothing. Prices live with the host, next to the routing policy they inform.
|
|
119
|
+
*
|
|
120
|
+
* `null` does NOT mean free — it means *unknown*, and the caller reports zero
|
|
121
|
+
* cost with `priced: false` so the gap stays visible rather than confidently
|
|
122
|
+
* wrong. (The removed built-in default priced every unrecognised model at
|
|
123
|
+
* Sonnet's rate, overstating a budget model's output by ~54×.)
|
|
124
|
+
*/
|
|
125
|
+
export function resolvePricing( model: string, hostPrices?: PriceTable ): ModelPrice | null {
|
|
126
|
+
if( !hostPrices ) return null
|
|
127
|
+
|
|
128
|
+
const exact = hostPrices[ model ]
|
|
129
|
+
if( exact ) return exact
|
|
130
|
+
|
|
131
|
+
// A host table keyed by bare ids still matches a dated / provider-prefixed
|
|
132
|
+
// model id, and vice versa.
|
|
133
|
+
const norm = normalizeModelKey( model )
|
|
134
|
+
if( hostPrices[ norm ] ) return hostPrices[ norm ]
|
|
135
|
+
|
|
136
|
+
for( const [ key, price ] of Object.entries( hostPrices ) ){
|
|
137
|
+
if( normalizeModelKey( key ) === norm ) return price
|
|
111
138
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
/** Resolve the pricing row for any model id (exact, normalized, then default). */
|
|
116
|
-
export function resolvePricing( model: string ): { input: number; output: number } {
|
|
117
|
-
return PRICING_BY_NORM[ normalizeModelKey( model ) ]
|
|
118
|
-
?? MODEL_PRICING[ model ]
|
|
119
|
-
?? MODEL_PRICING['__default__']!
|
|
139
|
+
|
|
140
|
+
return null
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
export interface TokenUsage {
|
|
123
144
|
/** Model identifier (e.g., 'openai/gpt-4o') */
|
|
124
145
|
model: string
|
|
146
|
+
/**
|
|
147
|
+
* The provider that actually served this call.
|
|
148
|
+
*
|
|
149
|
+
* Not derivable from `model`: routing is what makes the same model id
|
|
150
|
+
* reachable from several places — `deepseek-v3` direct, through a gateway, or
|
|
151
|
+
* self-hosted — at prices that differ by orders of magnitude. Without this a
|
|
152
|
+
* host billing across a multi-vendor routing table can attribute spend to a
|
|
153
|
+
* model but never to the vendor it actually paid.
|
|
154
|
+
*
|
|
155
|
+
* Optional because a caller recording usage directly (outside the LLM
|
|
156
|
+
* director) may not know it; absent means unattributed, not "the default".
|
|
157
|
+
*/
|
|
158
|
+
provider?: string
|
|
125
159
|
/** Input/prompt tokens consumed */
|
|
126
160
|
promptTokens: number
|
|
127
161
|
/** Output/completion tokens consumed */
|
|
@@ -132,16 +166,20 @@ export interface TokenUsage {
|
|
|
132
166
|
cacheReadTokens?: number
|
|
133
167
|
/** Anthropic prompt-cache write tokens (billed at 1.25× input). Optional. */
|
|
134
168
|
cacheWriteTokens?: number
|
|
135
|
-
/** Estimated cost in USD */
|
|
169
|
+
/** Estimated cost in USD. Zero when `priced` is false — unknown, not free. */
|
|
136
170
|
estimatedCostUsd: number
|
|
171
|
+
/**
|
|
172
|
+
* Whether a price was found for this model. False ⇒ `estimatedCostUsd` is 0
|
|
173
|
+
* because nothing priced it, NOT because the call was free. A consumer
|
|
174
|
+
* summing costs should surface unpriced calls rather than fold them in as
|
|
175
|
+
* zero.
|
|
176
|
+
*/
|
|
177
|
+
priced: boolean
|
|
137
178
|
|
|
138
179
|
// ── 5-axis cost attribution ──────────────────────────────
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
attribute: string
|
|
143
|
-
/** Cognitive function: 'decision' | 'ideation' | 'conversation' | 'planning' | 'deliberation' | 'outreach' | 'consolidation' | 'recall' | 'index' | 'identity-coherence' | … */
|
|
144
|
-
function: string
|
|
180
|
+
category: LLMCallCategory
|
|
181
|
+
attribute: LLMCallAttribute
|
|
182
|
+
function: LLMCallFunction
|
|
145
183
|
/** Optional specific id or namespace: facet id, entity id, model name. */
|
|
146
184
|
scope?: string
|
|
147
185
|
/** Human-readable label — auto-composed from the axes when the caller omits it. */
|
|
@@ -157,15 +195,21 @@ export interface TokenUsage {
|
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
/** What callers pass to {@link TokenTracker.recordUsage} — cost and label are derived. */
|
|
160
|
-
export type RecordUsageInput = Omit<TokenUsage, 'estimatedCostUsd' | 'label'> & { label?: string }
|
|
198
|
+
export type RecordUsageInput = Omit<TokenUsage, 'estimatedCostUsd' | 'label' | 'priced'> & { label?: string }
|
|
161
199
|
|
|
162
200
|
/** Compose a stable, readable label from the attribution axes. */
|
|
163
|
-
function composeLabel( m: { category:
|
|
201
|
+
function composeLabel( m: { category: LLMCallCategory; attribute: LLMCallAttribute; function: LLMCallFunction; scope?: string } ): string {
|
|
164
202
|
const base = `${m.category}/${m.attribute}/${m.function}`
|
|
165
203
|
return m.scope ? `${base}#${m.scope}` : base
|
|
166
204
|
}
|
|
167
205
|
|
|
168
206
|
export interface TokenTrackerConfig {
|
|
207
|
+
/**
|
|
208
|
+
* Host-supplied model prices (USD per 1M tokens), merged from the per-provider
|
|
209
|
+
* `prices` maps in `WillLLMConfig.providers`. These win over the built-in
|
|
210
|
+
* fallback table. Omitted ⇒ fallback only.
|
|
211
|
+
*/
|
|
212
|
+
prices?: PriceTable
|
|
169
213
|
/** Whether to emit cost events */
|
|
170
214
|
emitCostEvents?: boolean
|
|
171
215
|
/** Cost threshold for warning events */
|
|
@@ -186,6 +230,7 @@ export class TokenTracker implements SimulationEngine {
|
|
|
186
230
|
|
|
187
231
|
private _emitCostEvents: boolean
|
|
188
232
|
private _costWarningThreshold: number
|
|
233
|
+
private _prices: PriceTable | undefined
|
|
189
234
|
|
|
190
235
|
// All recorded usage for the simulation run
|
|
191
236
|
private _usageLog: TokenUsage[] = []
|
|
@@ -200,6 +245,11 @@ export class TokenTracker implements SimulationEngine {
|
|
|
200
245
|
private _categoryTokens = new Map<string, { prompt: number; completion: number }>()
|
|
201
246
|
private _functionCosts = new Map<string, number>()
|
|
202
247
|
private _functionTokens = new Map<string, { prompt: number; completion: number }>()
|
|
248
|
+
// Per-provider spend. The axis a host actually reconciles against invoices —
|
|
249
|
+
// "which vendor did we pay?" is not answerable from the model id once routing
|
|
250
|
+
// can reach one model through several of them.
|
|
251
|
+
private _providerCosts = new Map<string, number>()
|
|
252
|
+
private _providerTokens = new Map<string, { prompt: number; completion: number }>()
|
|
203
253
|
|
|
204
254
|
// Per-tick costs (for spike detection)
|
|
205
255
|
private _tickCosts: number[] = []
|
|
@@ -219,6 +269,7 @@ export class TokenTracker implements SimulationEngine {
|
|
|
219
269
|
constructor( config: TokenTrackerConfig = {} ){
|
|
220
270
|
this._emitCostEvents = config.emitCostEvents ?? true
|
|
221
271
|
this._costWarningThreshold = config.costWarningThresholdUsd ?? 0.05
|
|
272
|
+
this._prices = config.prices
|
|
222
273
|
this._ledgerPath = ( config.writeLedger && config.willId )
|
|
223
274
|
? `./data/wills/${config.willId}/debug/token-report.jsonl`
|
|
224
275
|
: null
|
|
@@ -231,18 +282,31 @@ export class TokenTracker implements SimulationEngine {
|
|
|
231
282
|
* Called by LLMDirector.call after each completion (src/llm/index.ts).
|
|
232
283
|
*/
|
|
233
284
|
recordUsage( usage: RecordUsageInput ): void {
|
|
234
|
-
const pricing = resolvePricing( usage.model )
|
|
285
|
+
const pricing = resolvePricing( usage.model, this._prices )
|
|
235
286
|
const cacheRead = usage.cacheReadTokens ?? 0
|
|
236
287
|
const cacheWrite = usage.cacheWriteTokens ?? 0
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(
|
|
288
|
+
|
|
289
|
+
// No price ⇒ cost 0 and `priced: false`. Warn once per model id so an
|
|
290
|
+
// unconfigured provider is visible without flooding the log.
|
|
291
|
+
if( !pricing && !_unpricedWarned.has( usage.model ) ){
|
|
292
|
+
_unpricedWarned.add( usage.model )
|
|
293
|
+
logger.warn(
|
|
294
|
+
`[tokens] no price for "${usage.model}" — reporting cost 0 for it. ` +
|
|
295
|
+
`Supply one via llm.providers.<provider>.prices to get cost telemetry.`
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const costUsd = pricing
|
|
300
|
+
? ( usage.promptTokens / 1_000_000 ) * pricing.input +
|
|
301
|
+
( usage.completionTokens / 1_000_000 ) * pricing.output +
|
|
302
|
+
( cacheRead / 1_000_000 ) * pricing.input * CACHE_READ_MULT +
|
|
303
|
+
( cacheWrite / 1_000_000 ) * pricing.input * CACHE_WRITE_MULT
|
|
304
|
+
: 0
|
|
242
305
|
|
|
243
306
|
const full: TokenUsage = {
|
|
244
307
|
...usage,
|
|
245
308
|
label: usage.label ?? composeLabel( usage ),
|
|
309
|
+
priced: pricing !== null,
|
|
246
310
|
estimatedCostUsd: Math.round( costUsd * 1_000_000 ) / 1_000_000, // round to micro-dollars
|
|
247
311
|
}
|
|
248
312
|
|
|
@@ -256,6 +320,9 @@ export class TokenTracker implements SimulationEngine {
|
|
|
256
320
|
// Per-axis breakdowns — the repartition surface (category × function).
|
|
257
321
|
this._accumulate( this._categoryCosts, this._categoryTokens, full.category, full )
|
|
258
322
|
this._accumulate( this._functionCosts, this._functionTokens, full.function, full )
|
|
323
|
+
// Unattributed rather than guessed: a caller that did not say which
|
|
324
|
+
// provider served the call must not be silently folded into the default.
|
|
325
|
+
this._accumulate( this._providerCosts, this._providerTokens, full.provider ?? 'unattributed', full )
|
|
259
326
|
|
|
260
327
|
// Complete attributed ledger record (every call, all axes + cost): notify
|
|
261
328
|
// record listeners (the stem forwards them onto the transport) and mirror to
|
|
@@ -280,6 +347,7 @@ export class TokenTracker implements SimulationEngine {
|
|
|
280
347
|
tick: full.tick,
|
|
281
348
|
ts: new Date( wallClock() ).toISOString(), // determinism-ok: ledger timestamp is telemetry, never replay state
|
|
282
349
|
model: full.model,
|
|
350
|
+
provider: full.provider,
|
|
283
351
|
category: full.category,
|
|
284
352
|
attribute: full.attribute,
|
|
285
353
|
function: full.function,
|
|
@@ -291,6 +359,10 @@ export class TokenTracker implements SimulationEngine {
|
|
|
291
359
|
cacheWriteTok: full.cacheWriteTokens ?? 0,
|
|
292
360
|
estPromptTok: full.estPromptTokens,
|
|
293
361
|
costUsd: full.estimatedCostUsd,
|
|
362
|
+
// Whether costUsd came from a real price. False ⇒ 0 because nothing
|
|
363
|
+
// priced this model, NOT because the call was free — a consumer summing
|
|
364
|
+
// spend must not fold unpriced calls in as zero.
|
|
365
|
+
priced: full.priced,
|
|
294
366
|
latencyMs: full.latencyMs,
|
|
295
367
|
}
|
|
296
368
|
|
|
@@ -347,31 +419,34 @@ export class TokenTracker implements SimulationEngine {
|
|
|
347
419
|
if( this._tickCosts.length > this._maxTickCostSamples )
|
|
348
420
|
this._tickCosts.shift()
|
|
349
421
|
|
|
350
|
-
// Metrics
|
|
422
|
+
// Metrics — TOKENS ONLY (W8c).
|
|
423
|
+
//
|
|
424
|
+
// Token counts are a physical, deterministic fact of a call and belong in
|
|
425
|
+
// state. Dollars are the host's accounting over that fact: prices differ per
|
|
426
|
+
// account, change on a vendor's schedule, and are ~0 self-hosted. Nothing in
|
|
427
|
+
// cognition ever read them (the sole consumer was a console runner), yet
|
|
428
|
+
// while they sat in state a host editing its price table changed state bytes
|
|
429
|
+
// and broke replay-equivalence over a number that influenced nothing.
|
|
430
|
+
//
|
|
431
|
+
// Cost still reaches the host every call on the ledger path
|
|
432
|
+
// (`onRecord` → the stem's transport bridge), which is where it was already
|
|
433
|
+
// being consumed. See `totalCostUsd` / `costBreakdown()` for in-process reads.
|
|
351
434
|
commands.metrics!.push(
|
|
352
435
|
[ 'llm.prompt_tokens_total', this._totalPromptTokens ],
|
|
353
436
|
[ 'llm.completion_tokens_total', this._totalCompletionTokens ],
|
|
354
|
-
[ 'llm.cost_total_usd', this._totalCost ],
|
|
355
|
-
[ 'llm.cost_this_tick_usd', tickCost ],
|
|
356
|
-
[ 'llm.cost_avg_per_tick_usd', this._averageTickCost() ],
|
|
357
437
|
[ 'llm.total_calls', this._usageLog.length ],
|
|
358
438
|
)
|
|
359
439
|
|
|
360
|
-
// Per-axis
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
commands.metrics!.push([ `llm.cost.${cat}`, cost ])
|
|
365
|
-
}
|
|
440
|
+
// Per-axis TOKEN breakdown — the transparency surface for "how much goes
|
|
441
|
+
// into conversation vs executive vs embedding". The matching cost
|
|
442
|
+
// breakdown is host-side now (W8c); `costBreakdown()` still exposes it
|
|
443
|
+
// in-process for anyone holding the tracker.
|
|
366
444
|
for( const [ cat, tok ] of this._categoryTokens ){
|
|
367
445
|
commands.metrics!.push(
|
|
368
446
|
[ `llm.prompt_tokens.${cat}`, tok.prompt ],
|
|
369
447
|
[ `llm.completion_tokens.${cat}`, tok.completion ],
|
|
370
448
|
)
|
|
371
449
|
}
|
|
372
|
-
for( const [ fn, cost ] of this._functionCosts ){
|
|
373
|
-
commands.metrics!.push([ `llm.cost.fn.${fn}`, cost ])
|
|
374
|
-
}
|
|
375
450
|
|
|
376
451
|
// Cost warning event
|
|
377
452
|
if( tickCost > this._costWarningThreshold
|
|
@@ -427,6 +502,24 @@ export class TokenTracker implements SimulationEngine {
|
|
|
427
502
|
return this._functionTokens
|
|
428
503
|
}
|
|
429
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Cost broken down by provider ('anthropic' | 'glm' | 'moonshot' | …), plus
|
|
507
|
+
* an `unattributed` bucket for usage recorded without one.
|
|
508
|
+
*
|
|
509
|
+
* This is the axis a host reconciles against vendor invoices. Calls whose
|
|
510
|
+
* model went unpriced contribute 0 here, so compare against
|
|
511
|
+
* `getUsageLog()`'s `priced` flag before treating a small number as a small
|
|
512
|
+
* bill.
|
|
513
|
+
*/
|
|
514
|
+
get providerBreakdown(): ReadonlyMap<string, number> {
|
|
515
|
+
return this._providerCosts
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Token counts (prompt + completion) broken down by provider. */
|
|
519
|
+
get providerTokenBreakdown(): ReadonlyMap<string, { prompt: number; completion: number }> {
|
|
520
|
+
return this._providerTokens
|
|
521
|
+
}
|
|
522
|
+
|
|
430
523
|
/** Cost per call average */
|
|
431
524
|
get averageCostPerCall(): number {
|
|
432
525
|
if( this._usageLog.length === 0 ) return 0
|
|
@@ -461,6 +554,8 @@ export class TokenTracker implements SimulationEngine {
|
|
|
461
554
|
this._categoryTokens.clear()
|
|
462
555
|
this._functionCosts.clear()
|
|
463
556
|
this._functionTokens.clear()
|
|
557
|
+
this._providerCosts.clear()
|
|
558
|
+
this._providerTokens.clear()
|
|
464
559
|
this._tickCosts = []
|
|
465
560
|
}
|
|
466
561
|
|
package/src/host/boot.ts
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
// WILL_NAME display name (default "Will")
|
|
13
13
|
// WILL_IDENTITY persona prompt (default a minimal self)
|
|
14
14
|
// WILL_TIER basic | standard | full (default standard)
|
|
15
|
-
// WILL_LLM mock |
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// WILL_LLM mock | any provider name (default: auto — whichever
|
|
16
|
+
// provider's own key is set,
|
|
17
|
+
// else the zero-key mock)
|
|
18
|
+
// WILL_LLM_MODEL concrete model id (REQUIRED for a live mind —
|
|
19
|
+
// the engine has no default)
|
|
18
20
|
// WILL_TICK_MS ms per tick (default 1000)
|
|
19
21
|
// WILL_SEED deterministic seed (testing) (default unseeded/wall-time)
|
|
20
22
|
// WILL_PMA_PATH PMA artifact path (default ./.will/<name>.pma.json)
|
|
@@ -25,10 +27,10 @@
|
|
|
25
27
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
26
28
|
import { dirname, resolve } from 'node:path'
|
|
27
29
|
import { setLogger } from '#core/logger'
|
|
28
|
-
import { Will, type CreateWillOptions } from '#sdk/will'
|
|
30
|
+
import { Will, detectProvider, type CreateWillOptions } from '#sdk/will'
|
|
29
31
|
import type { PMASnapshot } from '#pma/index'
|
|
30
32
|
import { connectMcpEffectors, type McpToolsSource } from '#root/mcp/effectors'
|
|
31
|
-
import { anthropicWireHeaders, defaultBaseFor,
|
|
33
|
+
import { anthropicWireHeaders, defaultBaseFor, knownWireFor, providerKeyFromEnv, PROVIDER_KEY_ENV, type LLMProvider, type LLMWire } from '#llm/index'
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Route every engine log line to stderr. For `will mcp`, stdout is the MCP
|
|
@@ -47,19 +49,51 @@ function slug( s: string ): string {
|
|
|
47
49
|
|
|
48
50
|
/** The LLM mode the hosts will boot with: an explicit WILL_LLM, else whichever
|
|
49
51
|
* provider's key is present, else the zero-key mock. */
|
|
50
|
-
export function resolveLlmMode(): 'mock' |
|
|
51
|
-
const explicit = process.env.WILL_LLM
|
|
52
|
-
if( explicit ) return explicit
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
export function resolveLlmMode(): 'mock' | LLMProvider {
|
|
53
|
+
const explicit = process.env.WILL_LLM
|
|
54
|
+
if( explicit ) return explicit as 'mock' | LLMProvider
|
|
55
|
+
// The SDK's own detection, not a copy of it. Boot used to know only
|
|
56
|
+
// anthropic/glm, so once the provider set widened, a Will booting live on
|
|
57
|
+
// (say) Kimi had its preflight silently skipped — the one check that exists
|
|
58
|
+
// to stop a mind that boots, perceives, and never speaks.
|
|
59
|
+
return detectProvider()
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/** The key for a live mode — the provider-agnostic override first, then the
|
|
59
63
|
* provider's own env. */
|
|
60
|
-
function resolveLlmKey( mode:
|
|
61
|
-
return process.env.WILL_LLM_API_KEY
|
|
62
|
-
|
|
64
|
+
function resolveLlmKey( mode: LLMProvider ): string | undefined {
|
|
65
|
+
return process.env.WILL_LLM_API_KEY ?? providerKeyFromEnv( mode )
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The smallest real completion request, in a given wire's dialect.
|
|
70
|
+
*
|
|
71
|
+
* `null` means "this wire has no cheap ping here" — the caller raises the mind
|
|
72
|
+
* unchecked rather than inventing a request shape and reading its rejection as
|
|
73
|
+
* a broken provider.
|
|
74
|
+
*/
|
|
75
|
+
export function pingRequest(
|
|
76
|
+
wire: LLMWire, base: string, model: string, key: string, provider: LLMProvider,
|
|
77
|
+
): { url: string; headers: Record<string, string>; body: unknown } | null {
|
|
78
|
+
const messages = [ { role: 'user', content: 'ping' } ]
|
|
79
|
+
switch( wire ){
|
|
80
|
+
case 'anthropic':
|
|
81
|
+
return {
|
|
82
|
+
url: `${ base }/messages`,
|
|
83
|
+
headers: anthropicWireHeaders( provider, key ),
|
|
84
|
+
body: { model, max_tokens: 1, messages },
|
|
85
|
+
}
|
|
86
|
+
case 'openai':
|
|
87
|
+
return {
|
|
88
|
+
url: `${ base }/chat/completions`,
|
|
89
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${ key }` },
|
|
90
|
+
body: { model, max_tokens: 1, messages },
|
|
91
|
+
}
|
|
92
|
+
// Gemini authenticates in the query string and nests its payload
|
|
93
|
+
// differently enough that a hand-rolled ping here would drift from the
|
|
94
|
+
// client that actually makes the calls. Better unchecked than wrong.
|
|
95
|
+
case 'google': return null
|
|
96
|
+
}
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
/**
|
|
@@ -83,7 +117,7 @@ async function preflightLLM( anatomy: string ): Promise<void> {
|
|
|
83
117
|
|
|
84
118
|
const key = resolveLlmKey( mode )
|
|
85
119
|
if( !key ){
|
|
86
|
-
const expected = mode
|
|
120
|
+
const expected = PROVIDER_KEY_ENV[ mode ] ?? 'WILL_LLM_API_KEY'
|
|
87
121
|
console.error(`[will] WILL_LLM=${ mode } but no ${ expected } / WILL_LLM_API_KEY is set.`)
|
|
88
122
|
console.error('[will] The Will would boot, perceive, and never speak. Set a key, or run keyless with WILL_LLM=mock.')
|
|
89
123
|
process.exit( 2 )
|
|
@@ -91,16 +125,38 @@ async function preflightLLM( anatomy: string ): Promise<void> {
|
|
|
91
125
|
|
|
92
126
|
// The ping validates key + balance + reachability, which is the failure class
|
|
93
127
|
// that strands an operator. It uses the pinned model when there is one, so a
|
|
94
|
-
// bad model id is caught too
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
// bad model id is caught too.
|
|
129
|
+
const base = process.env.WILL_LLM_BASE_URL ?? defaultBaseFor( mode )
|
|
130
|
+
if( !base ){
|
|
131
|
+
console.error(`[will] WILL_LLM=${ mode } has no known base URL — set WILL_LLM_BASE_URL.`)
|
|
132
|
+
process.exit( 2 )
|
|
133
|
+
}
|
|
134
|
+
// The ping needs *a* model id. WILL_LLM_MODEL is the one the Will will
|
|
135
|
+
// actually use, so preferring it means a bad id is caught here rather than at
|
|
136
|
+
// the first tick. Without it there is nothing honest to send: the engine no
|
|
137
|
+
// longer carries a default model, so say so plainly instead of guessing.
|
|
97
138
|
const model = process.env.WILL_LLM_MODEL
|
|
98
|
-
|
|
139
|
+
if( !model ){
|
|
140
|
+
console.error(`[will] WILL_LLM_MODEL is not set. The engine has no default model —`)
|
|
141
|
+
console.error('[will] pick one for your provider (e.g. WILL_LLM_MODEL=claude-sonnet-4-5-20250929).')
|
|
142
|
+
process.exit( 2 )
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Ping in the provider's OWN dialect. This used to be hardcoded to the
|
|
146
|
+
// Anthropic wire, which was fine when boot knew only anthropic and glm. With
|
|
147
|
+
// the provider set widened it became a false negative: an OpenAI-wire
|
|
148
|
+
// provider answers 404 to `/messages`, preflight read that as "the LLM
|
|
149
|
+
// refused", and a perfectly working Will never got raised.
|
|
150
|
+
const ping = pingRequest( knownWireFor( mode ) ?? 'openai', base, model, key, mode )
|
|
151
|
+
if( !ping ){
|
|
152
|
+
console.error(`[will] no preflight ping for the ${ mode } wire — raising the mind unchecked.`)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
99
155
|
try {
|
|
100
|
-
const res = await fetch(
|
|
156
|
+
const res = await fetch( ping.url, {
|
|
101
157
|
method: 'POST',
|
|
102
|
-
headers:
|
|
103
|
-
body: JSON.stringify(
|
|
158
|
+
headers: ping.headers,
|
|
159
|
+
body: JSON.stringify( ping.body ),
|
|
104
160
|
signal: AbortSignal.timeout( 20_000 ),
|
|
105
161
|
} )
|
|
106
162
|
if( res.ok ) return
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,41 @@ export {
|
|
|
26
26
|
|
|
27
27
|
export { assembleMind, type WillConfig } from '#stem/mind'
|
|
28
28
|
|
|
29
|
+
// Model routing — which model serves which call.
|
|
30
|
+
//
|
|
31
|
+
// These were reachable only through `WillConfig.llm.router`'s structural type,
|
|
32
|
+
// which meant the reference implementation this ships *for* hosts to use could
|
|
33
|
+
// not be imported by one. A seam nobody can import is not a seam.
|
|
34
|
+
//
|
|
35
|
+
// The engine carries mechanism only: a router sees what kind of work a call is
|
|
36
|
+
// and how much the moment demands, never who is paying or what anything costs.
|
|
37
|
+
// The table itself is the host's.
|
|
38
|
+
export {
|
|
39
|
+
NULL_ROUTER,
|
|
40
|
+
TableRouter,
|
|
41
|
+
chainRouters,
|
|
42
|
+
isNullRouter,
|
|
43
|
+
type ModelRouter,
|
|
44
|
+
type ModelRoute,
|
|
45
|
+
type RoutingRule,
|
|
46
|
+
} from '#llm/routing'
|
|
47
|
+
|
|
48
|
+
// Provider vocabulary — the wire each known provider speaks and where it lives.
|
|
49
|
+
// A provider outside this table works identically once the host declares its
|
|
50
|
+
// `wire` and `baseUrl` on the `llm.providers` entry.
|
|
51
|
+
export {
|
|
52
|
+
KNOWN_PROVIDERS,
|
|
53
|
+
knownWireFor,
|
|
54
|
+
defaultBaseFor,
|
|
55
|
+
BACKGROUND_DEMAND,
|
|
56
|
+
ESCALATION_DEMAND,
|
|
57
|
+
type LLMProvider,
|
|
58
|
+
type KnownProvider,
|
|
59
|
+
type LLMWire,
|
|
60
|
+
type LLMCallMeta,
|
|
61
|
+
type ProviderCredential,
|
|
62
|
+
} from '#llm/index'
|
|
63
|
+
|
|
29
64
|
// External transport — the unified bidirectional envelope channel between a Will
|
|
30
65
|
// and its host peer (backend). Exposes the transport interface, every envelope
|
|
31
66
|
// type, and the three implementations:
|