@chatpanel/events 0.102.0 → 0.104.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.
Files changed (139) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/model-ledger.js CHANGED
@@ -1,228 +1 @@
1
- // The MODEL LEDGER an engine's record, the scorecard pattern applied to engines
2
- // (architecture-pillars.md §13.2).
3
- //
4
- // One chained, store-attested ledger per ENGINE — keyed like the scorecard's `byEngine`
5
- // (`model:<provider>/<model>`, `harness:<id>`), because the same model at two providers is
6
- // two records: availability, cost and latency are the provider's, not the model's. Entries
7
- // are FACTS the runner and the gateway observe, never claims:
8
- //
9
- // call one turn: time to first token, total, tokens in/out, cost when priced, was
10
- // the JSON valid, were the tool calls valid, did it come back empty / refused
11
- // / truncated, did it succeed
12
- // declined it did not answer, and why (unavailable · auth · rate · credits · timeout ·
13
- // context) — the availability signal
14
- // rotated-from a task left it for another engine mid-run
15
- // rating a task's verdict, attributed to the engine that served it (and to the agent)
16
- // capability a proof: it was asked for X and it did / did not deliver
17
- // price what a token costs here — from the provider's list or typed by a person
18
- //
19
- // `summarizeEngine(entries)` → the ENGINE CARD: availability, reliability, latency, cost,
20
- // capability proofs (a capability with three failed proofs is WITHDRAWN until a person
21
- // re-enables it), quality by job kind, the last refs. model-candidates.js `applyCard` hands
22
- // the card to `applyOverride`: observed quality / latency / cost replace the name-based
23
- // guess wherever there is enough history (≥ `minCalls`), the guess stays as the prior until
24
- // then, and the result says which it used (`observed[]`). Reach is never learned, only
25
- // typed — a ledger cannot move a model closer than the URL says.
26
- //
27
- // Hashing, attestation and chain verification are scorecard.js's, unchanged: the same store
28
- // marks both, the same `verifyChain` checks both.
29
-
30
- import { canonical, sha256, engineKey, normalizeEngine } from './scorecard.js';
31
- export { verifyChain, attest, verifyAttested } from './scorecard.js';
32
-
33
- export const LEDGER_VERSION = 1;
34
- export const LEDGER_ENTRY_KINDS = Object.freeze(['call', 'declined', 'rotated-from', 'rating', 'capability', 'price']);
35
- export const DECLINE_REASONS = Object.freeze(['unavailable', 'auth', 'rate', 'credits', 'timeout', 'context', 'other']);
36
- export const STRUCTURED = Object.freeze(['ok', 'bad', 'n/a']);
37
- /** Failed proofs before a capability leaves the card. */
38
- export const WITHDRAW_AFTER = 3;
39
- /** Calls before an observed number outranks the name-based guess. Small; configurable. */
40
- export const DEFAULT_MIN_CALLS = 5;
41
-
42
- const n0 = (v) => Math.max(0, Math.round(Number(v) || 0));
43
- const money = (v) => (v == null || v === '' || !Number.isFinite(Number(v)) ? undefined : Math.max(0, Number(v)));
44
- const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
45
- const bool = (v) => v === true;
46
- const str = (v, n = 120) => (v == null || v === '' ? undefined : String(v).slice(0, n));
47
- const strip = (o) => { for (const k of Object.keys(o)) if (o[k] === undefined) delete o[k]; return o; };
48
-
49
- /** The ledger's key for an engine — the scorecard's `engineKey`, so the two join. */
50
- export function ledgerKey(engine) { return engineKey(engine); }
51
-
52
- /** A call fact, normalized. Rates are computed later; here every field is a plain observation. */
53
- export function normalizeCall(c) {
54
- const src = c && typeof c === 'object' ? c : {};
55
- return strip({
56
- ok: src.ok !== false,
57
- ttftMs: src.ttftMs != null ? n0(src.ttftMs) : undefined,
58
- totalMs: src.totalMs != null ? n0(src.totalMs) : undefined,
59
- tokensIn: src.tokensIn != null ? n0(src.tokensIn) : undefined,
60
- tokensOut: src.tokensOut != null ? n0(src.tokensOut) : undefined,
61
- // The total when the split is unknown (a harness reports one number, or none).
62
- tokens: src.tokens != null && src.tokensIn == null && src.tokensOut == null ? n0(src.tokens) : undefined,
63
- cost: money(src.cost),
64
- structured: STRUCTURED.includes(src.structured) ? src.structured : 'n/a',
65
- toolCalls: src.toolCalls && typeof src.toolCalls === 'object' ? { asked: n0(src.toolCalls.asked), valid: Math.min(n0(src.toolCalls.asked), n0(src.toolCalls.valid)) } : undefined,
66
- empty: bool(src.empty) || undefined,
67
- refused: bool(src.refused) || undefined,
68
- truncated: bool(src.truncated) || undefined,
69
- });
70
- }
71
-
72
- /**
73
- * A new entry chained onto `prev`. `fact.engine` is required and keyed; the rest is by kind.
74
- * Pure apart from the digest; the store attests.
75
- */
76
- export async function makeLedgerEntry(fact, prev, { now = () => Date.now(), subtle } = {}) {
77
- if (!fact || typeof fact !== 'object') throw new Error('model-ledger: an entry needs a fact');
78
- if (!LEDGER_ENTRY_KINDS.includes(fact.kind)) throw new Error(`model-ledger: kind must be one of ${LEDGER_ENTRY_KINDS.join(', ')}`);
79
- const engine = normalizeEngine(fact.engine);
80
- if (!engine) throw new Error('model-ledger: engine required');
81
- const key = engineKey(engine);
82
- if (prev && prev.key !== key) throw new Error(`model-ledger: entry for ${key} chained onto ${prev.key}`);
83
- const e = strip({
84
- v: LEDGER_VERSION,
85
- seq: prev ? prev.seq + 1 : 0,
86
- key,
87
- engine,
88
- kind: fact.kind,
89
- at: Number(fact.at) || now(),
90
- runId: str(fact.runId), taskId: str(fact.taskId), agentId: str(fact.agentId), jobKind: str(fact.jobKind, 60),
91
- call: fact.kind === 'call' ? normalizeCall(fact.call) : undefined,
92
- declined: fact.kind === 'declined' ? { reason: DECLINE_REASONS.includes(fact.declined?.reason) ? fact.declined.reason : 'other', ...(fact.declined?.error ? { error: String(fact.declined.error).slice(0, 300) } : {}) } : undefined,
93
- rotated: fact.kind === 'rotated-from' ? strip({ to: engineKey(fact.rotated?.to) || undefined, reason: str(fact.rotated?.reason, 300) }) : undefined,
94
- rating: fact.kind === 'rating' ? strip({ by: String(fact.rating?.by || 'person').slice(0, 40), score: clamp01(fact.rating?.score), jobKind: str(fact.rating?.jobKind || fact.jobKind, 60), agentId: str(fact.rating?.agentId || fact.agentId) }) : undefined,
95
- capability: fact.kind === 'capability' ? { id: String(fact.capability?.id || '').slice(0, 40), proved: bool(fact.capability?.proved) } : undefined,
96
- price: fact.kind === 'price' ? strip({ per1kIn: money(fact.price?.per1kIn) ?? 0, per1kOut: money(fact.price?.per1kOut) ?? 0, source: fact.price?.source === 'user' ? 'user' : 'provider', currency: str(fact.price?.currency, 8) }) : undefined,
97
- refs: Array.isArray(fact.refs) && fact.refs.length ? fact.refs.map(String).slice(0, 12) : undefined,
98
- prev: prev ? prev.hash : null,
99
- });
100
- if (e.kind === 'capability' && !e.capability.id) throw new Error('model-ledger: capability.id required');
101
- const { hash: _h, sig: _s, ...hashable } = e;
102
- e.hash = await sha256(canonical(hashable), { subtle });
103
- return e;
104
- }
105
-
106
- const percentile = (xs, p) => {
107
- if (!xs.length) return null;
108
- const s = [...xs].sort((a, b) => a - b);
109
- return s[Math.min(s.length - 1, Math.max(0, Math.ceil((p / 100) * s.length) - 1))];
110
- };
111
- const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null);
112
- const rate = (n, of) => (of ? Math.round((n / of) * 1000) / 1000 : null);
113
- const r3 = (v) => (v == null ? null : Math.round(v * 1000) / 1000);
114
-
115
- /**
116
- * The engine card. `minCalls` marks it `observed` once there is enough history; `now`
117
- * bounds the by-hour availability band (the last 24 h) and the "declining right now" check.
118
- */
119
- export function summarizeEngine(entries, { minCalls = DEFAULT_MIN_CALLS, now = Date.now(), recent = 5 } = {}) {
120
- const list = (entries || []).filter((e) => e && e.kind);
121
- const calls = list.filter((e) => e.kind === 'call');
122
- const declines = list.filter((e) => e.kind === 'declined');
123
- const attempts = calls.length + declines.length;
124
- // Availability: declines over attempts, and the last 24 hours in bands of one.
125
- const byHour = Array.from({ length: 24 }, () => ({ calls: 0, declines: 0 }));
126
- for (const e of [...calls, ...declines]) {
127
- const h = Math.floor((now - e.at) / 3600000);
128
- if (h >= 0 && h < 24) byHour[23 - h][e.kind === 'call' ? 'calls' : 'declines'] += 1;
129
- }
130
- const declinesBy = {};
131
- for (const e of declines) declinesBy[e.declined.reason] = (declinesBy[e.declined.reason] || 0) + 1;
132
- // Declining right now: the last three attempts all declined, within the last hour.
133
- const lastThree = [...calls, ...declines].sort((a, b) => a.at - b.at).slice(-3);
134
- const decliningNow = lastThree.length === 3 && lastThree.every((e) => e.kind === 'declined' && now - e.at < 3600000);
135
- // Reliability: each a rate over the calls it applies to.
136
- const withJson = calls.filter((e) => e.call.structured !== 'n/a');
137
- const withTools = calls.filter((e) => e.call.toolCalls?.asked);
138
- const reliability = {
139
- failRate: rate(calls.filter((e) => !e.call.ok).length, calls.length),
140
- empty: rate(calls.filter((e) => e.call.empty).length, calls.length),
141
- refused: rate(calls.filter((e) => e.call.refused).length, calls.length),
142
- truncated: rate(calls.filter((e) => e.call.truncated).length, calls.length),
143
- badJson: rate(withJson.filter((e) => e.call.structured === 'bad').length, withJson.length),
144
- badToolCall: rate(withTools.reduce((n, e) => n + (e.call.toolCalls.asked - e.call.toolCalls.valid), 0), withTools.reduce((n, e) => n + e.call.toolCalls.asked, 0)),
145
- };
146
- // Latency.
147
- const ttft = calls.map((e) => e.call.ttftMs).filter((v) => v != null);
148
- const total = calls.map((e) => e.call.totalMs).filter((v) => v != null);
149
- const latency = { ttft: { p50: percentile(ttft, 50), p95: percentile(ttft, 95), n: ttft.length }, total: { p50: percentile(total, 50), p95: percentile(total, 95), n: total.length } };
150
- // Cost: the latest price entry prices every call that reported tokens; a call that
151
- // reported its own cost is taken as is; otherwise the mean tokens per call stands in.
152
- const price = list.filter((e) => e.kind === 'price').at(-1)?.price || null;
153
- const costs = calls.map((e) => (e.call.cost != null ? e.call.cost : price && (e.call.tokensIn != null || e.call.tokensOut != null) ? ((e.call.tokensIn || 0) * price.per1kIn + (e.call.tokensOut || 0) * price.per1kOut) / 1000 : null)).filter((v) => v != null);
154
- const tokens = calls.map((e) => (e.call.tokensIn || 0) + (e.call.tokensOut || 0) + (e.call.tokens || 0)).filter((v) => v > 0);
155
- const cost = { perTask: r3(mean(costs)), priced: costs.length, tokensPerTask: tokens.length ? Math.round(mean(tokens)) : null, ...(price ? { per1kIn: price.per1kIn, per1kOut: price.per1kOut, source: price.source } : {}) };
156
- // Capability proofs: asked vs proved; withdrawn after WITHDRAW_AFTER failures unless a
157
- // later proof succeeded (a person re-enabling it is a proof they record).
158
- const proofs = {};
159
- for (const e of list.filter((x) => x.kind === 'capability')) {
160
- const p = proofs[e.capability.id] || (proofs[e.capability.id] = { asked: 0, proved: 0, failedSince: 0 });
161
- p.asked += 1;
162
- if (e.capability.proved) { p.proved += 1; p.failedSince = 0; } else p.failedSince += 1;
163
- }
164
- const capabilities = {
165
- proved: Object.keys(proofs).filter((id) => proofs[id].proved > 0 && proofs[id].failedSince < WITHDRAW_AFTER).sort(),
166
- withdrawn: Object.keys(proofs).filter((id) => proofs[id].failedSince >= WITHDRAW_AFTER).sort(),
167
- proofs: Object.fromEntries(Object.entries(proofs).map(([id, p]) => [id, { asked: p.asked, proved: p.proved }])),
168
- };
169
- // Quality: mean rating, overall and by job kind.
170
- const ratings = list.filter((e) => e.kind === 'rating');
171
- const byJobKind = {};
172
- for (const e of ratings) { const k = e.rating.jobKind || 'any'; (byJobKind[k] = byJobKind[k] || []).push(e.rating.score); }
173
- const quality = {
174
- overall: { avg: r3(mean(ratings.map((e) => e.rating.score))), count: ratings.length },
175
- byJobKind: Object.fromEntries(Object.entries(byJobKind).map(([k, xs]) => [k, { avg: r3(mean(xs)), count: xs.length }])),
176
- };
177
- const rotatedFrom = list.filter((e) => e.kind === 'rotated-from').length;
178
- return {
179
- key: list[0]?.key || null,
180
- engine: list[0]?.engine || null,
181
- entries: list.length,
182
- calls: calls.length,
183
- declines: declines.length,
184
- observed: calls.length >= minCalls,
185
- availability: { rate: attempts ? r3(1 - declines.length / attempts) : null, attempts, declinesBy, byHour, decliningNow },
186
- reliability,
187
- latency,
188
- cost,
189
- capabilities,
190
- quality,
191
- rotatedFrom,
192
- refs: [...new Set(list.flatMap((e) => e.refs || []))].slice(-recent),
193
- since: list[0]?.at || null,
194
- last: list.at(-1)?.at || null,
195
- head: list.at(-1)?.hash || null,
196
- };
197
- }
198
-
199
- /**
200
- * The override a card yields for model-candidates.js `applyOverride` — only the fields it
201
- * has enough history for. `quality` is the mean rating (for `jobKind` when the card has
202
- * ratings for it, else overall); `latencyMs` the observed p50 to first token (total when no
203
- * ttft was recorded); `costPer1k` from the price when one is known; `available: false`
204
- * only while it is declining right now. Returns `{ override, observed }`.
205
- *
206
- * Lives here, beside the card it reads, so recruit.js and a store without a router can use
207
- * it; `applyCard` (the override over the guess) stays in model-candidates.js beside
208
- * `applyOverride`, the seam it feeds.
209
- */
210
- export function cardOverride(card, { minCalls = DEFAULT_MIN_CALLS, jobKind = null } = {}) {
211
- const override = {}; const observed = [];
212
- if (!card) return { override, observed };
213
- const q = (jobKind && card.quality?.byJobKind?.[jobKind]?.count >= minCalls) ? card.quality.byJobKind[jobKind] : card.quality?.overall;
214
- if (q && q.count >= minCalls && q.avg != null) { override.quality = q.avg; observed.push('quality'); }
215
- const lat = card.latency?.ttft?.n >= minCalls ? card.latency.ttft.p50 : card.latency?.total?.n >= minCalls ? card.latency.total.p50 : null;
216
- if (lat != null) { override.latencyMs = lat; observed.push('latencyMs'); }
217
- // Six places, not three: a per-1k price is often 0.0004, and rounding it to 0 made a paid model read as free.
218
- if (card.cost?.per1kIn != null && card.cost?.per1kOut != null) { override.costPer1k = Math.round(((card.cost.per1kIn + card.cost.per1kOut) / 2) * 1e6) / 1e6; observed.push('costPer1k'); }
219
- if (card.availability?.decliningNow) { override.available = false; observed.push('available'); }
220
- return { override, observed };
221
- }
222
-
223
- // `applyCard` — the card over the name-based guess — lives in model-candidates.js beside
224
- // `applyOverride`, the seam it feeds; this module stays importable by a store that has no
225
- // router (the gateway vendors it with scorecard.js only).
226
- // Agent scores normalised by engine (§13.3) live beside the card they adjust: scorecard.js
227
- // `adjustSummary` and `fit(job, type, summary, { qualityOf })`.
228
- export { adjustSummary } from './scorecard.js';
1
+ import{canonical as U,sha256 as $,engineKey as w,normalizeEngine as G}from"./scorecard.js";import{verifyChain as oe,attest as re,verifyAttested as ie}from"./scorecard.js";const H=1,T=Object.freeze(["call","declined","rotated-from","rating","capability","price"]),W=Object.freeze(["unavailable","auth","rate","credits","timeout","context","other"]),B=Object.freeze(["ok","bad","n/a"]),R=3,A=5,c=e=>Math.max(0,Math.round(Number(e)||0)),S=e=>e==null||e===""||!Number.isFinite(Number(e))?void 0:Math.max(0,Number(e)),V=e=>Math.max(0,Math.min(1,Number(e)||0)),y=e=>e===!0,u=(e,n=120)=>e==null||e===""?void 0:String(e).slice(0,n),h=e=>{for(const n of Object.keys(e))e[n]===void 0&&delete e[n];return e};function X(e){return w(e)}function Y(e){const n=e&&typeof e=="object"?e:{};return h({ok:n.ok!==!1,ttftMs:n.ttftMs!=null?c(n.ttftMs):void 0,totalMs:n.totalMs!=null?c(n.totalMs):void 0,tokensIn:n.tokensIn!=null?c(n.tokensIn):void 0,tokensOut:n.tokensOut!=null?c(n.tokensOut):void 0,tokens:n.tokens!=null&&n.tokensIn==null&&n.tokensOut==null?c(n.tokens):void 0,cost:S(n.cost),structured:B.includes(n.structured)?n.structured:"n/a",toolCalls:n.toolCalls&&typeof n.toolCalls=="object"?{asked:c(n.toolCalls.asked),valid:Math.min(c(n.toolCalls.asked),c(n.toolCalls.valid))}:void 0,empty:y(n.empty)||void 0,refused:y(n.refused)||void 0,truncated:y(n.truncated)||void 0})}async function Z(e,n,{now:s=()=>Date.now(),subtle:a}={}){if(!e||typeof e!="object")throw new Error("model-ledger: an entry needs a fact");if(!T.includes(e.kind))throw new Error(`model-ledger: kind must be one of ${T.join(", ")}`);const o=G(e.engine);if(!o)throw new Error("model-ledger: engine required");const l=w(o);if(n&&n.key!==l)throw new Error(`model-ledger: entry for ${l} chained onto ${n.key}`);const i=h({v:H,seq:n?n.seq+1:0,key:l,engine:o,kind:e.kind,at:Number(e.at)||s(),runId:u(e.runId),taskId:u(e.taskId),agentId:u(e.agentId),jobKind:u(e.jobKind,60),call:e.kind==="call"?Y(e.call):void 0,declined:e.kind==="declined"?{reason:W.includes(e.declined?.reason)?e.declined.reason:"other",...e.declined?.error?{error:String(e.declined.error).slice(0,300)}:{}}:void 0,rotated:e.kind==="rotated-from"?h({to:w(e.rotated?.to)||void 0,reason:u(e.rotated?.reason,300)}):void 0,rating:e.kind==="rating"?h({by:String(e.rating?.by||"person").slice(0,40),score:V(e.rating?.score),jobKind:u(e.rating?.jobKind||e.jobKind,60),agentId:u(e.rating?.agentId||e.agentId)}):void 0,capability:e.kind==="capability"?{id:String(e.capability?.id||"").slice(0,40),proved:y(e.capability?.proved)}:void 0,price:e.kind==="price"?h({per1kIn:S(e.price?.per1kIn)??0,per1kOut:S(e.price?.per1kOut)??0,source:e.price?.source==="user"?"user":"provider",currency:u(e.price?.currency,8)}):void 0,refs:Array.isArray(e.refs)&&e.refs.length?e.refs.map(String).slice(0,12):void 0,prev:n?n.hash:null});if(i.kind==="capability"&&!i.capability.id)throw new Error("model-ledger: capability.id required");const{hash:g,sig:O,...f}=i;return i.hash=await $(U(f),{subtle:a}),i}const b=(e,n)=>{if(!e.length)return null;const s=[...e].sort((a,o)=>a-o);return s[Math.min(s.length-1,Math.max(0,Math.ceil(n/100*s.length)-1))]},m=e=>e.length?e.reduce((n,s)=>n+s,0)/e.length:null,k=(e,n)=>n?Math.round(e/n*1e3)/1e3:null,I=e=>e==null?null:Math.round(e*1e3)/1e3;function ee(e,{minCalls:n=A,now:s=Date.now(),recent:a=5}={}){const o=(e||[]).filter(t=>t&&t.kind),l=o.filter(t=>t.kind==="call"),i=o.filter(t=>t.kind==="declined"),g=l.length+i.length,O=Array.from({length:24},()=>({calls:0,declines:0}));for(const t of[...l,...i]){const r=Math.floor((s-t.at)/36e5);r>=0&&r<24&&(O[23-r][t.kind==="call"?"calls":"declines"]+=1)}const f={};for(const t of i)f[t.declined.reason]=(f[t.declined.reason]||0)+1;const x=[...l,...i].sort((t,r)=>t.at-r.at).slice(-3),D=x.length===3&&x.every(t=>t.kind==="declined"&&s-t.at<36e5),N=l.filter(t=>t.call.structured!=="n/a"),C=l.filter(t=>t.call.toolCalls?.asked),_={failRate:k(l.filter(t=>!t.call.ok).length,l.length),empty:k(l.filter(t=>t.call.empty).length,l.length),refused:k(l.filter(t=>t.call.refused).length,l.length),truncated:k(l.filter(t=>t.call.truncated).length,l.length),badJson:k(N.filter(t=>t.call.structured==="bad").length,N.length),badToolCall:k(C.reduce((t,r)=>t+(r.call.toolCalls.asked-r.call.toolCalls.valid),0),C.reduce((t,r)=>t+r.call.toolCalls.asked,0))},M=l.map(t=>t.call.ttftMs).filter(t=>t!=null),E=l.map(t=>t.call.totalMs).filter(t=>t!=null),L={ttft:{p50:b(M,50),p95:b(M,95),n:M.length},total:{p50:b(E,50),p95:b(E,95),n:E.length}},p=o.filter(t=>t.kind==="price").at(-1)?.price||null,q=l.map(t=>t.call.cost!=null?t.call.cost:p&&(t.call.tokensIn!=null||t.call.tokensOut!=null)?((t.call.tokensIn||0)*p.per1kIn+(t.call.tokensOut||0)*p.per1kOut)/1e3:null).filter(t=>t!=null),K=l.map(t=>(t.call.tokensIn||0)+(t.call.tokensOut||0)+(t.call.tokens||0)).filter(t=>t>0),z={perTask:I(m(q)),priced:q.length,tokensPerTask:K.length?Math.round(m(K)):null,...p?{per1kIn:p.per1kIn,per1kOut:p.per1kOut,source:p.source}:{}},d={};for(const t of o.filter(r=>r.kind==="capability")){const r=d[t.capability.id]||(d[t.capability.id]={asked:0,proved:0,failedSince:0});r.asked+=1,t.capability.proved?(r.proved+=1,r.failedSince=0):r.failedSince+=1}const J={proved:Object.keys(d).filter(t=>d[t].proved>0&&d[t].failedSince<R).sort(),withdrawn:Object.keys(d).filter(t=>d[t].failedSince>=R).sort(),proofs:Object.fromEntries(Object.entries(d).map(([t,r])=>[t,{asked:r.asked,proved:r.proved}]))},v=o.filter(t=>t.kind==="rating"),j={};for(const t of v){const r=t.rating.jobKind||"any";(j[r]=j[r]||[]).push(t.rating.score)}const F={overall:{avg:I(m(v.map(t=>t.rating.score))),count:v.length},byJobKind:Object.fromEntries(Object.entries(j).map(([t,r])=>[t,{avg:I(m(r)),count:r.length}]))},P=o.filter(t=>t.kind==="rotated-from").length;return{key:o[0]?.key||null,engine:o[0]?.engine||null,entries:o.length,calls:l.length,declines:i.length,observed:l.length>=n,availability:{rate:g?I(1-i.length/g):null,attempts:g,declinesBy:f,byHour:O,decliningNow:D},reliability:_,latency:L,cost:z,capabilities:J,quality:F,rotatedFrom:P,refs:[...new Set(o.flatMap(t=>t.refs||[]))].slice(-a),since:o[0]?.at||null,last:o.at(-1)?.at||null,head:o.at(-1)?.hash||null}}function te(e,{minCalls:n=A,jobKind:s=null}={}){const a={},o=[];if(!e)return{override:a,observed:o};const l=s&&e.quality?.byJobKind?.[s]?.count>=n?e.quality.byJobKind[s]:e.quality?.overall;l&&l.count>=n&&l.avg!=null&&(a.quality=l.avg,o.push("quality"));const i=e.latency?.ttft?.n>=n?e.latency.ttft.p50:e.latency?.total?.n>=n?e.latency.total.p50:null;return i!=null&&(a.latencyMs=i,o.push("latencyMs")),e.cost?.per1kIn!=null&&e.cost?.per1kOut!=null&&(a.costPer1k=Math.round((e.cost.per1kIn+e.cost.per1kOut)/2*1e6)/1e6,o.push("costPer1k")),e.availability?.decliningNow&&(a.available=!1,o.push("available")),{override:a,observed:o}}import{adjustSummary as ae}from"./scorecard.js";export{W as DECLINE_REASONS,A as DEFAULT_MIN_CALLS,T as LEDGER_ENTRY_KINDS,H as LEDGER_VERSION,B as STRUCTURED,R as WITHDRAW_AFTER,ae as adjustSummary,re as attest,te as cardOverride,X as ledgerKey,Z as makeLedgerEntry,Y as normalizeCall,ee as summarizeEngine,ie as verifyAttested,oe as verifyChain};
package/model-picker.js CHANGED
@@ -1,188 +1 @@
1
- // THE MODEL PICKER'S SHAPE one flat list of models in, the menu a person can read out.
2
- //
3
- // Every client is handed the same flat array by the gateway's /v1/models: fifty-odd entries
4
- // mixing local CLI agents with a dozen providers' cloud models, in whatever order the routing
5
- // table happened to enumerate them. Rendered literally that is a single scrolling column of
6
- // opaque ids — `claude-code`, `gpt-4o-mini`, `qwen2.5-coder:7b` — where the two things a user
7
- // actually distinguishes are invisible:
8
- //
9
- // • AN AGENT IS NOT A MODEL. A bridge-backed CLI runs on this machine, holds a session, can
10
- // touch files and costs a process; a cloud model is a stateless HTTP call. Picking between
11
- // them is a different decision from picking between two models, and a flat list forces
12
- // both decisions through one control.
13
- // • A PROVIDER IS THE UNIT PEOPLE THINK IN. "the Anthropic one", "my Ollama". Sorting by id
14
- // interleaves providers so the same provider's models are scattered down the list.
15
- //
16
- // So this turns the flat list into SECTIONS, and it lives here rather than in a client because
17
- // it is pure input → output with no window in it: the desktop renders it as a menu, the
18
- // extension as its agent menu, a mobile client as a grouped list, and all three group and
19
- // order identically. Writing it in one client is how `displayName` got duplicated.
20
- //
21
- // UNAVAILABLE MODELS ARE KEPT, NOT DROPPED. A CLI the user has not installed is the single
22
- // most common reason a first message fails, and a picker that silently omits it answers
23
- // "where did Claude Code go?" with nothing. It is listed, ordered last within its section,
24
- // flagged, and carries the reason — see `reason` on the entry.
25
-
26
- /** A bridge-backed CLI agent, as opposed to an HTTP model endpoint. */
27
- const isAgent = (m) => m?.providerType === 'agent' || m?.viaBridge === true;
28
-
29
- /**
30
- * The provider a model belongs to, in the words the gateway used.
31
- *
32
- * `provider` is what gateways from 0.6.64 report; `owned_by`/`owner` is the older field. A
33
- * name is NEVER parsed for a provider — reading `gpt-` as "OpenAI" breaks the moment someone
34
- * serves a GPT-named model from their own Ollama, and that user's whole point was that it is
35
- * local.
36
- */
37
- const providerOf = (m) => String(m?.provider || m?.owner || '').trim();
38
-
39
- /**
40
- * How a provider is spelled when a person wrote it down.
41
- *
42
- * Capitalising the first letter is right for `anthropic` and wrong for every brand with
43
- * internal capitals — it renders "Openai", "Deepseek", "Xai". A heading is the most-read text
44
- * in the picker, and misspelling the company in it reads as carelessness about everything
45
- * else, so the handful that do not follow the rule are simply listed.
46
- */
47
- const PROVIDER_NAMES = {
48
- openai: 'OpenAI', openrouter: 'OpenRouter', deepseek: 'DeepSeek', xai: 'xAI',
49
- vllm: 'vLLM', lmstudio: 'LM Studio', llamacpp: 'llama.cpp', 'llama.cpp': 'llama.cpp',
50
- huggingface: 'Hugging Face', togetherai: 'Together AI', together: 'Together AI',
51
- githubcopilot: 'GitHub Copilot', awsbedrock: 'AWS Bedrock', bedrock: 'AWS Bedrock',
52
- azureopenai: 'Azure OpenAI', googleai: 'Google AI', vertexai: 'Vertex AI',
53
- };
54
-
55
- /** Sentence case for a bare provider slug, leaving names that already have shape alone. */
56
- function providerLabel(raw) {
57
- const p = String(raw || '').trim();
58
- if (!p) return 'Other';
59
- const known = PROVIDER_NAMES[p.toLowerCase().replace(/[\s_-]/g, '')];
60
- if (known) return known;
61
- if (/[A-Z ]/.test(p)) return p; // already presentable: "Together AI", "MyCorp LLM"
62
- return p.charAt(0).toUpperCase() + p.slice(1);
63
- }
64
-
65
- const availableFirst = (a, b) => (
66
- (a.available === false) - (b.available === false)
67
- || String(a.id).localeCompare(String(b.id))
68
- );
69
-
70
- /**
71
- * Group a flat model list into the sections a picker draws.
72
- *
73
- * Sections come back in the order they should be shown: **Agents first**, then each provider
74
- * alphabetically. Agents lead because a local CLI is the one target a fresh install can be
75
- * sure of — no key, no account — which is the same reason `target-choice.js` defaults to one.
76
- *
77
- * @param models the gateway's models, each `{ id, provider, providerType, viaBridge,
78
- * available, reason, owner, label? }`
79
- * @param selectedId currently chosen id, so a caller can mark it without a second pass
80
- * @returns `[{ key, label, kind: 'agent'|'provider', items: [...] }]`, where each item is the
81
- * model object plus `{ label, selected }`. Empty sections are never emitted.
82
- */
83
- export function groupModels(models, { selectedId = '' } = {}) {
84
- const list = Array.isArray(models) ? models.filter(Boolean) : [];
85
-
86
- const agents = [];
87
- const byProvider = new Map();
88
- for (const m of list) {
89
- if (isAgent(m)) { agents.push(m); continue; }
90
- const key = providerOf(m) || 'other';
91
- if (!byProvider.has(key)) byProvider.set(key, []);
92
- byProvider.get(key).push(m);
93
- }
94
-
95
- const decorate = (m) => ({
96
- ...m,
97
- label: m.label || m.id,
98
- selected: !!selectedId && m.id === selectedId,
99
- });
100
-
101
- const sections = [];
102
- if (agents.length) {
103
- sections.push({
104
- key: 'agents',
105
- // Naming (naming-revamp.md): the CLI coding agents are AGENT TOOLS — a runtime one
106
- // of the user's own agents can be given as its engine. The key and kind are code.
107
- label: 'Agent Tools',
108
- kind: 'agent',
109
- items: agents.slice().sort(availableFirst).map(decorate),
110
- });
111
- }
112
- for (const key of [...byProvider.keys()].sort((a, b) => a.localeCompare(b))) {
113
- sections.push({
114
- key: `provider:${key}`,
115
- label: providerLabel(key),
116
- kind: 'provider',
117
- items: byProvider.get(key).slice().sort(availableFirst).map(decorate),
118
- });
119
- }
120
- return sections;
121
- }
122
-
123
- /**
124
- * Filter the grouped sections by a typed query, keeping the grouping.
125
- *
126
- * A provider name matches ALL of its models: someone typing "ollama" is asking to see that
127
- * provider, not to see the models whose ids happen to contain the string — and with local
128
- * models the id usually does not contain it at all.
129
- *
130
- * Sections that end up empty are dropped, so an unmatched provider does not leave a heading
131
- * floating over nothing.
132
- */
133
- export function filterSections(sections, query) {
134
- const q = String(query || '').trim().toLowerCase();
135
- if (!q) return sections;
136
- const out = [];
137
- for (const s of sections) {
138
- if (s.label.toLowerCase().includes(q)) { out.push(s); continue; }
139
- const items = s.items.filter((m) => (
140
- String(m.label).toLowerCase().includes(q) || String(m.id).toLowerCase().includes(q)
141
- ));
142
- if (items.length) out.push({ ...s, items });
143
- }
144
- return out;
145
- }
146
-
147
- /**
148
- * The id a picker should start on when the record does not name one.
149
- *
150
- * Prefers a REACHABLE agent, then any reachable model, and only then something known to be
151
- * unavailable. The rule is the product's, and it is the same one `target-choice.js` states:
152
- * never default to a target that cannot answer, because the user is the one who finds out.
153
- */
154
- export function defaultModelId(models) {
155
- const list = Array.isArray(models) ? models.filter(Boolean) : [];
156
- const usable = list.filter((m) => m.available !== false);
157
- return (usable.find(isAgent) || usable[0] || list[0] || {}).id || '';
158
- }
159
-
160
- /**
161
- * How to describe the chosen model in one line — what the picker's own button says.
162
- *
163
- * The three-valued `available` is the honesty rule applied to a button: an id that names
164
- * nothing is only BROKEN if there was a list for it to be absent from. Before /v1/models has
165
- * answered there is no such list, so the truthful answer is `null` — "nobody has looked yet"
166
- * — and a caller must not paint that as a red dot. Marking an unloaded picker unavailable is
167
- * the same mistake as drawing "0 redactions" when nothing was inspected.
168
- */
169
- export function modelSummary(models, id) {
170
- const list = Array.isArray(models) ? models.filter(Boolean) : [];
171
- const m = list.find((x) => x.id === id);
172
- if (!m) {
173
- return {
174
- label: id || 'No model',
175
- available: list.length ? false : null,
176
- reason: list.length && id ? `${id} is not offered by the gateway any more` : '',
177
- agent: false,
178
- provider: '',
179
- };
180
- }
181
- return {
182
- label: m.label || m.id,
183
- available: m.available !== false,
184
- reason: m.reason || '',
185
- agent: isAgent(m),
186
- provider: providerLabel(providerOf(m)),
187
- };
188
- }
1
+ const c=e=>e?.providerType==="agent"||e?.viaBridge===!0,p=e=>String(e?.provider||e?.owner||"").trim(),d={openai:"OpenAI",openrouter:"OpenRouter",deepseek:"DeepSeek",xai:"xAI",vllm:"vLLM",lmstudio:"LM Studio",llamacpp:"llama.cpp","llama.cpp":"llama.cpp",huggingface:"Hugging Face",togetherai:"Together AI",together:"Together AI",githubcopilot:"GitHub Copilot",awsbedrock:"AWS Bedrock",bedrock:"AWS Bedrock",azureopenai:"Azure OpenAI",googleai:"Google AI",vertexai:"Vertex AI"};function u(e){const t=String(e||"").trim();if(!t)return"Other";const o=d[t.toLowerCase().replace(/[\s_-]/g,"")];return o||(/[A-Z ]/.test(t)?t:t.charAt(0).toUpperCase()+t.slice(1))}const f=(e,t)=>(e.available===!1)-(t.available===!1)||String(e.id).localeCompare(String(t.id));function b(e,{selectedId:t=""}={}){const o=Array.isArray(e)?e.filter(Boolean):[],r=[],i=new Map;for(const a of o){if(c(a)){r.push(a);continue}const s=p(a)||"other";i.has(s)||i.set(s,[]),i.get(s).push(a)}const l=a=>({...a,label:a.label||a.id,selected:!!t&&a.id===t}),n=[];r.length&&n.push({key:"agents",label:"Agent Tools",kind:"agent",items:r.slice().sort(f).map(l)});for(const a of[...i.keys()].sort((s,g)=>s.localeCompare(g)))n.push({key:`provider:${a}`,label:u(a),kind:"provider",items:i.get(a).slice().sort(f).map(l)});return n}function h(e,t){const o=String(t||"").trim().toLowerCase();if(!o)return e;const r=[];for(const i of e){if(i.label.toLowerCase().includes(o)){r.push(i);continue}const l=i.items.filter(n=>String(n.label).toLowerCase().includes(o)||String(n.id).toLowerCase().includes(o));l.length&&r.push({...i,items:l})}return r}function A(e){const t=Array.isArray(e)?e.filter(Boolean):[],o=t.filter(r=>r.available!==!1);return(o.find(c)||o[0]||t[0]||{}).id||""}function v(e,t){const o=Array.isArray(e)?e.filter(Boolean):[],r=o.find(i=>i.id===t);return r?{label:r.label||r.id,available:r.available!==!1,reason:r.reason||"",agent:c(r),provider:u(p(r))}:{label:t||"No model",available:o.length?!1:null,reason:o.length&&t?`${t} is not offered by the gateway any more`:"",agent:!1,provider:""}}export{A as defaultModelId,h as filterSections,b as groupModels,v as modelSummary};