behavior-wrapped 0.10.1 → 0.11.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 +2 -1
- package/dist/assets/{index-BaiNv0uD.js → index-By4wq-jG.js} +2 -2
- package/dist/assets/{index-D-wey53w.css → index-CkqI9mbd.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/leaderboard.mjs +4 -1
- package/server/model-names.mjs +2 -2
- package/server/phrase-card.mjs +14 -4
- package/server/phrase-models.mjs +20 -0
- package/server/public-report-schema.mjs +2 -0
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="theme-color" content="#0d0b1b" />
|
|
7
7
|
<meta name="description" content="A local-first behavior report for you and your AI agents." />
|
|
8
8
|
<title>Behavior Wrapped</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-By4wq-jG.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CkqI9mbd.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/leaderboard.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BEHAVIOR_WRAPPED_ORIGIN } from "./origins.mjs";
|
|
2
2
|
import { buildSessionLengthDistribution } from "./session-length-distribution.mjs";
|
|
3
|
+
import { validatePhraseModels } from "./phrase-models.mjs";
|
|
3
4
|
export const LEADERBOARD_RELAY_ORIGIN = BEHAVIOR_WRAPPED_ORIGIN;
|
|
4
5
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
5
6
|
const demoTokens = [820_000, 2_400_000, 8_900_000, 14_300_000, 31_000_000, 47_500_000, 83_000_000, 126_000_000, 210_000_000, 380_000_000, 620_000_000, 940_000_000];
|
|
@@ -56,6 +57,7 @@ export function leaderboardAggregateFromReport(report) {
|
|
|
56
57
|
favorite_phrase: typeof phrase === "string" && /^[a-z]+(?:'[a-z]+)?(?: [a-z]+(?:'[a-z]+)?){3,9}$/.test(phrase) ? phrase : null,
|
|
57
58
|
phrase_occurrences: Math.round(finiteNonNegative(report?.phraseCard?.occurrences)),
|
|
58
59
|
phrase_sessions: Math.round(finiteNonNegative(report?.phraseCard?.distinctSessions)),
|
|
60
|
+
phrase_models: validatePhraseModels(report?.phraseCard?.sourceModels, Math.round(finiteNonNegative(report?.phraseCard?.occurrences))) || [],
|
|
59
61
|
session_turn_counts: sessionTurnCounts(stats.sessionTurnCounts),
|
|
60
62
|
};
|
|
61
63
|
}
|
|
@@ -92,7 +94,8 @@ export function syntheticLeaderboardSnapshot(aggregate, participation = null) {
|
|
|
92
94
|
distribution: buildSessionLengthDistribution(demoSessionTurnCounts.flat()),
|
|
93
95
|
},
|
|
94
96
|
phrases: {
|
|
95
|
-
entries: aggregate.favorite_phrase ? [{ participant_id: 1, phrase: aggregate.favorite_phrase, occurrences: aggregate.phrase_occurrences, sessions: aggregate.phrase_sessions }] : [],
|
|
97
|
+
entries: aggregate.favorite_phrase ? [{ participant_id: 1, phrase: aggregate.favorite_phrase, occurrences: aggregate.phrase_occurrences, sessions: aggregate.phrase_sessions, models: aggregate.phrase_models || [], participants: 1 }] : [],
|
|
98
|
+
common: [],
|
|
96
99
|
},
|
|
97
100
|
participation: participation || { joined: false },
|
|
98
101
|
};
|
package/server/model-names.mjs
CHANGED
|
@@ -3,8 +3,8 @@ export function displayModelName(value) {
|
|
|
3
3
|
if (raw === "<synthetic>") return "Synthetic model";
|
|
4
4
|
const claude = raw.match(/^claude-([a-z]+)-(\d+)-(\d+)$/i);
|
|
5
5
|
if (claude) return `Claude ${claude[1][0].toUpperCase()}${claude[1].slice(1).toLowerCase()} ${claude[2]}.${claude[3]}`;
|
|
6
|
-
const gpt = raw.match(/^gpt-(\d+)[.-](\d+)(?:-([a-z]+))?$/i);
|
|
7
|
-
if (gpt) return `GPT-${gpt[1]}
|
|
6
|
+
const gpt = raw.match(/^gpt-(\d+)(?:[.-](\d+))?(?:-([a-z]+))?$/i);
|
|
7
|
+
if (gpt) return `GPT-${gpt[1]}${gpt[2] ? `.${gpt[2]}` : ""}${gpt[3] ? ` ${gpt[3][0].toUpperCase()}${gpt[3].slice(1).toLowerCase()}` : ""}`;
|
|
8
8
|
return raw
|
|
9
9
|
.replace(/^claude-/i, "Claude ")
|
|
10
10
|
.replace(/^gpt-/i, "GPT-")
|
package/server/phrase-card.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
2
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
3
3
|
import { BEHAVIOR_WRAPPED_ORIGIN } from "./origins.mjs";
|
|
4
|
+
import { displayModelName } from "./model-names.mjs";
|
|
5
|
+
import { validatePhraseModels } from "./phrase-models.mjs";
|
|
4
6
|
|
|
5
7
|
export const OPENROUTER_MODEL = "openai/gpt-5.6-luna";
|
|
6
8
|
export const PHRASE_JUDGE_NAME = "GPT-5.6 Luna";
|
|
@@ -56,6 +58,9 @@ export function buildPhraseCandidates(sessionRecords, { maximumCandidates = MAX_
|
|
|
56
58
|
if (record.type !== "assistant" || record.isApiErrorMessage || record?.message?.model === "<synthetic>") continue;
|
|
57
59
|
const prose = cleanText(visibleText(record));
|
|
58
60
|
if (!prose.trim()) continue;
|
|
61
|
+
const rawModel = record?.message?.model || record?.model;
|
|
62
|
+
const modelName = displayModelName(typeof rawModel === "string" ? rawModel.replace(/^(?:openai|anthropic)\//i, "") : "Unknown model");
|
|
63
|
+
const model = /^[\p{L}\p{N} ._+-]{1,80}$/u.test(modelName) && !/^(?:Codex|Claude|Cowork|Unknown) model$/i.test(modelName) ? modelName : "Unknown model";
|
|
59
64
|
let clauseIndex = 0;
|
|
60
65
|
for (const part of segmenter.segment(prose)) {
|
|
61
66
|
const clauses = part.segment.split(/(?:[;:—–]|\n+|,(?=\s+(?:and|but|or|so|yet)\b))/i);
|
|
@@ -68,8 +73,10 @@ export function buildPhraseCandidates(sessionRecords, { maximumCandidates = MAX_
|
|
|
68
73
|
if (slice.filter((token) => !stopwords.has(token)).length < 2) continue;
|
|
69
74
|
const phrase = slice.join(" ");
|
|
70
75
|
let item = counts.get(phrase);
|
|
71
|
-
if (!item) counts.set(phrase, item = { phrase, occurrences: 0, sessions: new Set(), openingOccurrences: 0, startBoundaryOccurrences: 0, endBoundaryOccurrences: 0, previousTokens: new Map(), nextTokens: new Map() });
|
|
76
|
+
if (!item) counts.set(phrase, item = { phrase, occurrences: 0, models: new Map(), sessions: new Set(), openingOccurrences: 0, startBoundaryOccurrences: 0, endBoundaryOccurrences: 0, previousTokens: new Map(), nextTokens: new Map() });
|
|
72
77
|
item.occurrences++;
|
|
78
|
+
const sourceModel = item.models.has(model) || item.models.size < 49 ? model : "Unknown model";
|
|
79
|
+
item.models.set(sourceModel, (item.models.get(sourceModel) || 0) + 1);
|
|
73
80
|
item.sessions.add(sessionIndex);
|
|
74
81
|
if (clauseIndex === 0 && offset === 0) item.openingOccurrences++;
|
|
75
82
|
if (offset === 0) item.startBoundaryOccurrences++;
|
|
@@ -114,6 +121,7 @@ export function buildPhraseCandidates(sessionRecords, { maximumCandidates = MAX_
|
|
|
114
121
|
phrase: item.phrase,
|
|
115
122
|
occurrences: item.occurrences,
|
|
116
123
|
distinct_sessions: item.sessions.size,
|
|
124
|
+
sourceModels: [...item.models].map(([model, count]) => ({ model, count })).sort((left, right) => right.count - left.count || left.model.localeCompare(right.model)),
|
|
117
125
|
opening_rate: Number((item.openingOccurrences / item.occurrences).toFixed(4)),
|
|
118
126
|
start_boundary_rate: Number((item.startBoundaryOccurrences / item.occurrences).toFixed(4)),
|
|
119
127
|
end_boundary_rate: Number((item.endBoundaryOccurrences / item.occurrences).toFixed(4)),
|
|
@@ -151,7 +159,7 @@ export function extractCandidateId(body, candidates) {
|
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
export function buildOpenRouterJudgeRequest(candidates, model = OPENROUTER_MODEL) {
|
|
154
|
-
const payload = JSON.stringify(candidates);
|
|
162
|
+
const payload = JSON.stringify(candidates.map(({ sourceModels, ...candidate }) => candidate));
|
|
155
163
|
assertSafePayload(payload);
|
|
156
164
|
return {
|
|
157
165
|
model,
|
|
@@ -188,6 +196,7 @@ function phraseCardFromSelection(candidates, candidateId, { model, provider, lat
|
|
|
188
196
|
phrase: selected.phrase,
|
|
189
197
|
occurrences: selected.occurrences,
|
|
190
198
|
distinctSessions: selected.distinct_sessions,
|
|
199
|
+
sourceModels: validatePhraseModels(selected.sourceModels, selected.occurrences) || [],
|
|
191
200
|
model,
|
|
192
201
|
provider,
|
|
193
202
|
latencyMs,
|
|
@@ -254,7 +263,8 @@ export async function judgePhraseCard(candidates, apiKey, { fetchImpl = fetch, m
|
|
|
254
263
|
|
|
255
264
|
export async function judgePhraseCardViaRelay(candidates, { fetchImpl = fetch, endpoint = PHRASE_JUDGE_RELAY_URL, clientId, timeoutMs = PHRASE_JUDGE_TIMEOUT_MS } = {}) {
|
|
256
265
|
if (!candidates.length) throw new Error("Not enough repeated, share-safe phrases were found for a phrase card.");
|
|
257
|
-
const
|
|
266
|
+
const judgeCandidates = candidates.map(({ sourceModels, ...candidate }) => candidate);
|
|
267
|
+
const payload = JSON.stringify(judgeCandidates);
|
|
258
268
|
assertSafePayload(payload);
|
|
259
269
|
const startedAt = Date.now();
|
|
260
270
|
const debug = judgeRequestDetails("favorite-phrase", "relay", endpoint, candidates);
|
|
@@ -268,7 +278,7 @@ export async function judgePhraseCardViaRelay(candidates, { fetchImpl = fetch, e
|
|
|
268
278
|
...(clientId ? { "x-behavior-wrapped-client": clientId } : {}),
|
|
269
279
|
},
|
|
270
280
|
signal: AbortSignal.timeout(timeoutMs),
|
|
271
|
-
body: JSON.stringify({ candidates }),
|
|
281
|
+
body: JSON.stringify({ candidates: judgeCandidates }),
|
|
272
282
|
});
|
|
273
283
|
} catch (error) {
|
|
274
284
|
const wrapped = timeoutMessage(error, timeoutMs);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Counts describe the models that said the selected phrase, not the selection judge.
|
|
2
|
+
export function validatePhraseModels(value, total) {
|
|
3
|
+
if (value === undefined) return [];
|
|
4
|
+
if (!Array.isArray(value) || value.length > 50) return null;
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const models = [];
|
|
7
|
+
for (const item of value) {
|
|
8
|
+
const model = typeof item?.model === "string" ? item.model.normalize("NFKC").trim() : "";
|
|
9
|
+
if (!/^[\p{L}\p{N} ._+-]{1,80}$/u.test(model) || seen.has(model)
|
|
10
|
+
|| !Number.isInteger(item?.count) || item.count < 1 || item.count > 10_000_000) return null;
|
|
11
|
+
seen.add(model);
|
|
12
|
+
models.push({ model, count: item.count });
|
|
13
|
+
}
|
|
14
|
+
if (models.length && models.reduce((sum, item) => sum + item.count, 0) !== total) return null;
|
|
15
|
+
return models.sort((left, right) => right.count - left.count || left.model.localeCompare(right.model));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parsePhraseModels(value, total) {
|
|
19
|
+
try { return validatePhraseModels(JSON.parse(value), total) || []; } catch { return []; }
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isShareSafeFrustrationQuote } from "./frustration-card.mjs";
|
|
2
2
|
import { safeWorkaroundSummary } from "./instrumental-workarounds.mjs";
|
|
3
|
+
import { validatePhraseModels } from "./phrase-models.mjs";
|
|
3
4
|
|
|
4
5
|
function safeNumber(value, maximum = 10_000_000_000_000) {
|
|
5
6
|
const number = Number(value);
|
|
@@ -101,6 +102,7 @@ export function sanitizePublicReport(value) {
|
|
|
101
102
|
phrase,
|
|
102
103
|
occurrences: Math.round(safeNumber(value.phraseCard.occurrences, 10_000_000)),
|
|
103
104
|
distinctSessions: Math.round(safeNumber(value.phraseCard.distinctSessions, 1_000_000)),
|
|
105
|
+
sourceModels: validatePhraseModels(value.phraseCard.sourceModels, Math.round(safeNumber(value.phraseCard.occurrences, 10_000_000))) || [],
|
|
104
106
|
} : null;
|
|
105
107
|
const frustrationQuote = value.interactionCard?.frustrationQuote || value.interactionCard?.quote;
|
|
106
108
|
const allowedLanguages = new Set(["English", "Spanish", "French", "German", "Portuguese", "Italian", "Japanese", "Korean", "Chinese", "Arabic", "Hebrew", "Hindi", "Thai", "Cyrillic"]);
|