@chatpanel/events 0.101.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -348
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/model-ledger.js
CHANGED
|
@@ -1,228 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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};
|