@bongos/core 1.19.176 → 1.19.178
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/.bongos-core.json +22 -17
- package/modules/economy/credits.js +67 -0
- package/modules/economy/reward.js +2 -0
- package/modules/ideas/module.json +16 -5
- package/modules/ideas/routes/inbox.js +26 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/llm-pricing.js +37 -8
- package/src/module-api.js +5 -1
- package/tests/cost_reconcile.mjs +30 -0
- package/tests/idea_full_pass_credit.mjs +113 -0
- package/tests/llm-pricing.mjs +68 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.178",
|
|
6
|
+
"core_contract": "1.19.178",
|
|
7
|
+
"source_commit": "039017134d5f7aee3cc1df34e7238847f878d51c",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-16T20:
|
|
9
|
+
"built_at": "2026-08-16T20:52:11.709Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 306,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1274,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1601,
|
|
20
|
+
"tree_sha256": "c2eb9f930075434353ce4f1fccd3189b5a30229ff5083e7fd9b4f7ee57508e45",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2832,7 +2832,7 @@
|
|
|
2832
2832
|
{
|
|
2833
2833
|
"path": "modules/economy/credits.js",
|
|
2834
2834
|
"mode": "0000644",
|
|
2835
|
-
"sha256": "
|
|
2835
|
+
"sha256": "b6f8c6fcabe6cb9f5eb83e936d129509004c86969d5e0d16a9653ff06b88f7ac"
|
|
2836
2836
|
},
|
|
2837
2837
|
{
|
|
2838
2838
|
"path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
|
|
@@ -2847,7 +2847,7 @@
|
|
|
2847
2847
|
{
|
|
2848
2848
|
"path": "modules/economy/reward.js",
|
|
2849
2849
|
"mode": "0000644",
|
|
2850
|
-
"sha256": "
|
|
2850
|
+
"sha256": "2a354daf06e344a613c8ad7df12ed6e534a9920f49b9f62b54e29b64ead59546"
|
|
2851
2851
|
},
|
|
2852
2852
|
{
|
|
2853
2853
|
"path": "modules/economy/routes/cost.js",
|
|
@@ -3477,7 +3477,7 @@
|
|
|
3477
3477
|
{
|
|
3478
3478
|
"path": "modules/ideas/module.json",
|
|
3479
3479
|
"mode": "0000644",
|
|
3480
|
-
"sha256": "
|
|
3480
|
+
"sha256": "64116b6abe3b2e13e3e6697b5c53e12534de698376410e2a14e319d4836481be"
|
|
3481
3481
|
},
|
|
3482
3482
|
{
|
|
3483
3483
|
"path": "modules/ideas/routes/blockers.js",
|
|
@@ -3487,7 +3487,7 @@
|
|
|
3487
3487
|
{
|
|
3488
3488
|
"path": "modules/ideas/routes/inbox.js",
|
|
3489
3489
|
"mode": "0000644",
|
|
3490
|
-
"sha256": "
|
|
3490
|
+
"sha256": "d6307869ae1b4388aa33244a1807f74ca2442122dea597e2f1ea5b7fe4533495"
|
|
3491
3491
|
},
|
|
3492
3492
|
{
|
|
3493
3493
|
"path": "modules/ideas/templates.js",
|
|
@@ -4227,12 +4227,12 @@
|
|
|
4227
4227
|
{
|
|
4228
4228
|
"path": "package-lock.json",
|
|
4229
4229
|
"mode": "0000644",
|
|
4230
|
-
"sha256": "
|
|
4230
|
+
"sha256": "1048f3d5207f880d23989332e8eebcaf55edcbd117046b2e72e6ce65f9fc5f42"
|
|
4231
4231
|
},
|
|
4232
4232
|
{
|
|
4233
4233
|
"path": "package.json",
|
|
4234
4234
|
"mode": "0000644",
|
|
4235
|
-
"sha256": "
|
|
4235
|
+
"sha256": "8792a5baa25590aacff1a8ff0ea6690fce9d800d1ac1ce74fc64f998e2038c3c"
|
|
4236
4236
|
},
|
|
4237
4237
|
{
|
|
4238
4238
|
"path": "public-docs/index.html",
|
|
@@ -5642,7 +5642,7 @@
|
|
|
5642
5642
|
{
|
|
5643
5643
|
"path": "src/bongos/llm-pricing.js",
|
|
5644
5644
|
"mode": "0000644",
|
|
5645
|
-
"sha256": "
|
|
5645
|
+
"sha256": "f7688e75a873f9c287f6559311877081830798cb1b5f1e548ceff3fcbed9e31d"
|
|
5646
5646
|
},
|
|
5647
5647
|
{
|
|
5648
5648
|
"path": "src/bongos/metrics.js",
|
|
@@ -5837,7 +5837,7 @@
|
|
|
5837
5837
|
{
|
|
5838
5838
|
"path": "src/module-api.js",
|
|
5839
5839
|
"mode": "0000644",
|
|
5840
|
-
"sha256": "
|
|
5840
|
+
"sha256": "56077ebaf7d23e7e524497ae087473677b8a7ca34e5695ccbf0a54544884be83"
|
|
5841
5841
|
},
|
|
5842
5842
|
{
|
|
5843
5843
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6367,7 +6367,7 @@
|
|
|
6367
6367
|
{
|
|
6368
6368
|
"path": "tests/cost_reconcile.mjs",
|
|
6369
6369
|
"mode": "0000644",
|
|
6370
|
-
"sha256": "
|
|
6370
|
+
"sha256": "8356250d8fc753afb2960d867f81586531053afd15f4e66da1a1b7b62b6b5db7"
|
|
6371
6371
|
},
|
|
6372
6372
|
{
|
|
6373
6373
|
"path": "tests/cost_summary_shape.mjs",
|
|
@@ -6964,6 +6964,11 @@
|
|
|
6964
6964
|
"mode": "0000644",
|
|
6965
6965
|
"sha256": "95b53f1acfa61f723741b4d33b5e882525903ccc549ae01f3094a788251a60ec"
|
|
6966
6966
|
},
|
|
6967
|
+
{
|
|
6968
|
+
"path": "tests/idea_full_pass_credit.mjs",
|
|
6969
|
+
"mode": "0000644",
|
|
6970
|
+
"sha256": "1ae98ae22202e6a6dbf42a48e0133654eeda32eec36b697c59f6b9d630936817"
|
|
6971
|
+
},
|
|
6967
6972
|
{
|
|
6968
6973
|
"path": "tests/idea_grades.mjs",
|
|
6969
6974
|
"mode": "0000644",
|
|
@@ -7102,7 +7107,7 @@
|
|
|
7102
7107
|
{
|
|
7103
7108
|
"path": "tests/llm-pricing.mjs",
|
|
7104
7109
|
"mode": "0000644",
|
|
7105
|
-
"sha256": "
|
|
7110
|
+
"sha256": "de3fabaeafb3eb3ad500becf565f3b6d7118a763f1e80d085f94b6ba990d5c58"
|
|
7106
7111
|
},
|
|
7107
7112
|
{
|
|
7108
7113
|
"path": "tests/local_preview.mjs",
|
|
@@ -107,6 +107,17 @@ const KIND_MULTIPLIERS = Object.freeze({
|
|
|
107
107
|
});
|
|
108
108
|
const IDEA_BONUS_FRACTION = 0.25; // 25% slice to the idea's original capturer
|
|
109
109
|
|
|
110
|
+
// =========================================================================
|
|
111
|
+
// The IDEATOR credit lane — stream 2 (ADR 0172 / goal 1000067 IC2).
|
|
112
|
+
// =========================================================================
|
|
113
|
+
// A Full Idea that clears the completeness bar pays its author a flat credit,
|
|
114
|
+
// ONCE. This is the ideator lane, NOT the builder per-task estimate — it gates
|
|
115
|
+
// on config.ideatorCredit, never perTaskCredit, so cost-plus-only does not
|
|
116
|
+
// suppress it (that is the whole point of ADR 0172). The other two streams
|
|
117
|
+
// (per-task-ship IC4, governance-pass IC5) will be siblings of this function.
|
|
118
|
+
const IDEA_FULL_PASS_CREDIT = 5; // owner: "gives 5 credits"
|
|
119
|
+
const IDEA_FULL_PASS_BAR = 60; // completeness 0-100; ADR 0172 proposed default
|
|
120
|
+
|
|
110
121
|
// =========================================================================
|
|
111
122
|
// Session token reward (flat cost-plus) — task 1005 (ADR 0054).
|
|
112
123
|
// =========================================================================
|
|
@@ -321,6 +332,59 @@ async function maybeAwardIdeaPromotionBonus(client, taskId, shipperBuilderId, mu
|
|
|
321
332
|
};
|
|
322
333
|
}
|
|
323
334
|
|
|
335
|
+
// Stream 2 (ADR 0172 / IC2): a Full Idea with a passing completeness score pays
|
|
336
|
+
// its author IDEA_FULL_PASS_CREDIT, ONCE. Transaction-participant like the rest
|
|
337
|
+
// of the reward port — the caller passes its `client` (a pool works too, since
|
|
338
|
+
// the write is a single idempotent statement).
|
|
339
|
+
//
|
|
340
|
+
// client — pg client or pool
|
|
341
|
+
// ideaId, authorId — the idea and its captured_by builder
|
|
342
|
+
// completenessScore — the R142 score (0-100) or null (unscorable)
|
|
343
|
+
// config — resolved reward policy; gates on config.ideatorCredit
|
|
344
|
+
//
|
|
345
|
+
// Returns { credited, reason }:
|
|
346
|
+
// { credited: 5, reason: 'paid' } — first pass, booked
|
|
347
|
+
// { credited: 0, reason: 'already_paid' } — a prior row for this idea exists
|
|
348
|
+
// { credited: 0, reason: 'below_bar' } — null or < IDEA_FULL_PASS_BAR
|
|
349
|
+
// { credited: 0, reason: 'ideator_lane_off' } — instance opted the lane out
|
|
350
|
+
//
|
|
351
|
+
// IDEMPOTENCY is the reason string: `idea.credit.full_pass:<ideaId>` is unique
|
|
352
|
+
// per idea, and the INSERT ... WHERE NOT EXISTS is a single atomic statement, so
|
|
353
|
+
// a re-grade / re-capture / concurrent request can never double-pay — no unique
|
|
354
|
+
// constraint or extra column on the core credit_log table needed (which a module
|
|
355
|
+
// migration may not add anyway). task_id is NULL: an idea credit has no task.
|
|
356
|
+
async function awardIdeaFullPassCredit(client, { ideaId, authorId, completenessScore, config = rewardConfig() } = {}) {
|
|
357
|
+
if (config && config.ideatorCredit === false) {
|
|
358
|
+
return { credited: 0, reason: 'ideator_lane_off' };
|
|
359
|
+
}
|
|
360
|
+
if (completenessScore == null || !Number.isFinite(Number(completenessScore)) || Number(completenessScore) < IDEA_FULL_PASS_BAR) {
|
|
361
|
+
return { credited: 0, reason: 'below_bar' };
|
|
362
|
+
}
|
|
363
|
+
if (authorId == null || ideaId == null) {
|
|
364
|
+
return { credited: 0, reason: 'missing_author_or_idea' };
|
|
365
|
+
}
|
|
366
|
+
const reason = `idea.credit.full_pass:${ideaId}`;
|
|
367
|
+
const description = `Full Idea #${ideaId} passed (completeness ${Math.round(Number(completenessScore))}/100) → ${IDEA_FULL_PASS_CREDIT}c to its author (ideator lane, ADR 0172)`;
|
|
368
|
+
// Idempotency predicate is `builder_id = author AND reason = <per-idea key>`.
|
|
369
|
+
// The `builder_id` term is NOT for correctness — the reason string is already
|
|
370
|
+
// globally unique per idea — it exists to hit the existing
|
|
371
|
+
// `idx_credit_log_builder (builder_id, recorded_at)` index: the NOT EXISTS
|
|
372
|
+
// narrows to the author's handful of credit rows (index scan) instead of a
|
|
373
|
+
// sequential scan of a credit_log that grows with total platform activity.
|
|
374
|
+
// credit_log is a CORE table, so a module migration cannot add an index on
|
|
375
|
+
// `reason` (the additive-namespace rule) — reusing the builder index is the
|
|
376
|
+
// in-scope fix. (R143/IC2 grader finding.)
|
|
377
|
+
const { rowCount } = await client.query(
|
|
378
|
+
`INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
|
|
379
|
+
SELECT $1, NULL, $2, $3, $4
|
|
380
|
+
WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $3)`,
|
|
381
|
+
[authorId, IDEA_FULL_PASS_CREDIT, reason, description]
|
|
382
|
+
);
|
|
383
|
+
return rowCount > 0
|
|
384
|
+
? { credited: IDEA_FULL_PASS_CREDIT, reason: 'paid' }
|
|
385
|
+
: { credited: 0, reason: 'already_paid' };
|
|
386
|
+
}
|
|
387
|
+
|
|
324
388
|
// Award the flat cost-plus session token reward, idempotent per session_id
|
|
325
389
|
// (task 1005, ADR 0054). Called inside the same transaction as the session
|
|
326
390
|
// upsert (see upsertSessionRecordWithReward).
|
|
@@ -531,6 +595,9 @@ module.exports = {
|
|
|
531
595
|
applyKindMultiplier,
|
|
532
596
|
insertMultipliedConfirmCredit,
|
|
533
597
|
maybeAwardIdeaPromotionBonus,
|
|
598
|
+
awardIdeaFullPassCredit,
|
|
599
|
+
IDEA_FULL_PASS_CREDIT,
|
|
600
|
+
IDEA_FULL_PASS_BAR,
|
|
534
601
|
awardSessionTokenReward,
|
|
535
602
|
NET_NEGATIVE_BONUS,
|
|
536
603
|
SUBTRACTOR_THRESHOLD,
|
|
@@ -24,6 +24,8 @@ module.exports = {
|
|
|
24
24
|
// credit allocation — transaction-participant: caller passes its tx `client`
|
|
25
25
|
insertMultipliedConfirmCredit: (...a) => credits.insertMultipliedConfirmCredit(...a),
|
|
26
26
|
maybeAwardIdeaPromotionBonus: (...a) => credits.maybeAwardIdeaPromotionBonus(...a),
|
|
27
|
+
// ADR 0172 IC2 — the ideator credit lane (stream 2): a passing Full Idea pays its author, once.
|
|
28
|
+
awardIdeaFullPassCredit: (...a) => credits.awardIdeaFullPassCredit(...a),
|
|
27
29
|
awardNetNegativeBonus: (...a) => credits.awardNetNegativeBonus(...a),
|
|
28
30
|
awardSessionTokenReward: (...a) => credits.awardSessionTokenReward(...a),
|
|
29
31
|
// gamification hooks the lifecycle fires at claim/ship
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"key": "ideas",
|
|
3
3
|
"title": "Ideas",
|
|
4
|
-
"description": "The idea inbox (capture, triage, criterion-proposal ratification) and the blockers queue
|
|
4
|
+
"description": "The idea inbox (capture, triage, criterion-proposal ratification) and the blockers queue \u2014 the methodology surfaces that feed the backlog. A core-domain module carved out of the GDS core in BV1.R78 (ADR 0093 \u00a71); default-on because every instance captures ideas and tracks what's stuck on its owner. Registers the `ideas.capture` kernel PORT the discord #ideas inbound resolves (ADR 0093 \u00a75 \u2014 retiring the temporary `captureIdea` doorway leak).",
|
|
5
5
|
"coreVersion": "^1.8.0",
|
|
6
6
|
"default": true,
|
|
7
|
-
"maintenance": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
7
|
+
"maintenance": {
|
|
8
|
+
"status": "core-maintained"
|
|
9
|
+
},
|
|
10
|
+
"contributes": {
|
|
11
|
+
"routes": [
|
|
12
|
+
"inbox",
|
|
13
|
+
"blockers"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"provides": [
|
|
17
|
+
"ideas.capture"
|
|
18
|
+
],
|
|
19
|
+
"consumes": [
|
|
20
|
+
"reward"
|
|
21
|
+
]
|
|
11
22
|
}
|
|
@@ -120,6 +120,32 @@ module.exports = function buildInboxRouter() {
|
|
|
120
120
|
idea.template_key = row.template_key;
|
|
121
121
|
idea.field_values = row.field_values;
|
|
122
122
|
idea.completeness_score = row.completeness_score;
|
|
123
|
+
|
|
124
|
+
// Stream 2 (ADR 0172 / IC2): a Full Idea that clears the completeness
|
|
125
|
+
// bar pays its author, once, on the IDEATOR credit lane — a separate
|
|
126
|
+
// reward from the builder estimate, so cost-plus-only does NOT
|
|
127
|
+
// suppress it. Resolved through the economy `reward` port (this
|
|
128
|
+
// module declares `reward` in its consumes), never a direct import.
|
|
129
|
+
// FAIL-OPEN, inside the same guard: a credit hiccup must never lose
|
|
130
|
+
// the capture — the idea is already stored. The port method is
|
|
131
|
+
// idempotent (reason = idea.credit.full_pass:<id>), so this cannot
|
|
132
|
+
// double-pay on a re-capture. The pool is the tx client — the write
|
|
133
|
+
// is one atomic idempotent statement.
|
|
134
|
+
try {
|
|
135
|
+
const reward = api.resolveOptional && api.resolveOptional('reward');
|
|
136
|
+
if (reward && reward.awardIdeaFullPassCredit) {
|
|
137
|
+
const r = await reward.awardIdeaFullPassCredit(api.pool, {
|
|
138
|
+
ideaId: idea.id,
|
|
139
|
+
authorId: req.builder.id,
|
|
140
|
+
completenessScore: row.completeness_score,
|
|
141
|
+
});
|
|
142
|
+
if (r && r.credited > 0) idea.ideator_credit = r.credited;
|
|
143
|
+
}
|
|
144
|
+
} catch (creditErr) {
|
|
145
|
+
console.error('[gds] POST /inbox full-idea pass-credit failed (idea captured + graded, credit skipped):', creditErr);
|
|
146
|
+
// no warning surfaced — the idea and its grade are intact; the credit
|
|
147
|
+
// is the only thing missed, and a reconcile/re-grade path can pay it.
|
|
148
|
+
}
|
|
123
149
|
} catch (gradeErr) {
|
|
124
150
|
console.error('[gds] POST /inbox full-grade write failed (idea captured, landing as quick):', gradeErr);
|
|
125
151
|
grade_warning = 'captured_but_ungraded';
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.178",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.178",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.178",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -34,20 +34,42 @@
|
|
|
34
34
|
// Family-based pricing assumes the CURRENT model in each family (what this
|
|
35
35
|
// instance runs). A historical id like claude-opus-4-1 ($15/$75) would be
|
|
36
36
|
// mis-priced low here — acceptable: the platform only runs 4.x-current models.
|
|
37
|
+
//
|
|
38
|
+
// VERIFIED 2026-08-16 against the live Anthropic pricing page (task 1002883):
|
|
39
|
+
// added the `fable` family. familyFor() previously matched only opus/sonnet/
|
|
40
|
+
// haiku, so claude-fable-5 — the owner's top-model default and this instance's
|
|
41
|
+
// `assumed_builder_model` (modules/grading/grader-rubric.json) — fell through to
|
|
42
|
+
// null and was priced at the opus fallback with unknown_model=true. Fable is
|
|
43
|
+
// 2× opus, so every cost-plus reward computed from a Fable session was priced at
|
|
44
|
+
// HALF its true cost (not conservative: cost-plus UNDER-pays the builder). The
|
|
45
|
+
// grading side already carried the family token (grader.js MODEL_FAMILY_TOKENS);
|
|
46
|
+
// this table is the surface that never caught up.
|
|
47
|
+
// - fable = Claude Fable 5 / Mythos 5 ($10 in / $50 out) — identical rates,
|
|
48
|
+
// which is why one family token covers both.
|
|
49
|
+
// - opus = Claude Opus 5 / 4.8 / 4.7 ($5 in / $25 out) [re-verified]
|
|
50
|
+
// - sonnet = Claude Sonnet 4.6 ($3 in / $15 out) [re-verified]
|
|
51
|
+
// - haiku = Claude Haiku 4.5 ($1 in / $5 out) [re-verified]
|
|
52
|
+
// NOTE (deliberately NOT changed here — its own task, per the rule above):
|
|
53
|
+
// Claude Sonnet 5 is now $2/$10, not the $3/$15 this `sonnet` row carries. The
|
|
54
|
+
// grader's default worker model is claude-sonnet-5, so grade costs are currently
|
|
55
|
+
// OVER-priced ~1.5×. That is a separate price-table decision; see task 1003001.
|
|
37
56
|
const PRICES = {
|
|
38
57
|
// family : { input, output, cache_write_5m, cache_write_1h, cache_read } (USD / 1e6 tokens)
|
|
58
|
+
fable: { input: 10, output: 50, cache_write_5m: 12.5, cache_write_1h: 20, cache_read: 1 },
|
|
39
59
|
opus: { input: 5, output: 25, cache_write_5m: 6.25, cache_write_1h: 10, cache_read: 0.5 },
|
|
40
60
|
sonnet: { input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 },
|
|
41
61
|
haiku: { input: 1, output: 5, cache_write_5m: 1.25, cache_write_1h: 2, cache_read: 0.1 },
|
|
42
62
|
};
|
|
43
63
|
|
|
44
|
-
// Resolve a model id (e.g. 'claude-
|
|
45
|
-
// 'claude-haiku-4-5') to a price family by substring —
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
64
|
+
// Resolve a model id (e.g. 'claude-fable-5', 'claude-opus-4-8',
|
|
65
|
+
// 'claude-sonnet-4-6', 'claude-haiku-4-5') to a price family by substring —
|
|
66
|
+
// mirrors grader.js's MODEL_FAMILY_TOKENS convention, including the fable/mythos
|
|
67
|
+
// pair it checks first (all live Claude ids contain one of these tokens).
|
|
68
|
+
// Unknown → null (caller decides: we price at the opus rate AND flag it, so an
|
|
69
|
+
// unpriced model is loud, never silently $0 — see priceUsage on why opus).
|
|
49
70
|
function familyFor(model) {
|
|
50
71
|
const m = String(model || '').toLowerCase();
|
|
72
|
+
if (m.includes('fable') || m.includes('mythos')) return 'fable';
|
|
51
73
|
if (m.includes('opus')) return 'opus';
|
|
52
74
|
if (m.includes('sonnet')) return 'sonnet';
|
|
53
75
|
if (m.includes('haiku')) return 'haiku';
|
|
@@ -70,7 +92,14 @@ function priceUsage(model, usage) {
|
|
|
70
92
|
const u = usage || {};
|
|
71
93
|
const fam = familyFor(model);
|
|
72
94
|
const unknownModel = fam === null;
|
|
73
|
-
|
|
95
|
+
// Unrecognized → price at the opus rate. Since `fable` landed (task 1002883)
|
|
96
|
+
// opus is no longer the ceiling, so this is a MID-range fallback, not a
|
|
97
|
+
// conservative one: an unknown Fable-class model prices at half its true rate.
|
|
98
|
+
// That is why unknown_model=true rides along — the loudness is the safeguard,
|
|
99
|
+
// not the rate. (Deliberately left at opus: every model this platform actually
|
|
100
|
+
// runs is now matched, so raising the fallback would only over-price genuinely
|
|
101
|
+
// foreign ids like gpt-4o.)
|
|
102
|
+
const priceFamily = fam || 'opus';
|
|
74
103
|
const p = PRICES[priceFamily];
|
|
75
104
|
|
|
76
105
|
const input = Number(u.input_tokens) || 0; // uncached input
|
|
@@ -179,7 +208,7 @@ function priceModelUsage(modelUsage) {
|
|
|
179
208
|
* Like priceModelUsage, model_usage has NO 5m/1h cache-creation breakdown — the
|
|
180
209
|
* `cache_creation` lump is priced at the 5m rate, exactly as session-cost.js
|
|
181
210
|
* priceTranscript does in its `else` branch (no breakdown → 5m). Unknown model
|
|
182
|
-
* ids are
|
|
211
|
+
* ids are priced as opus AND collected, so an unpriced model is
|
|
183
212
|
* loud, never silently $0. Pure: no I/O, no Date.now().
|
|
184
213
|
*
|
|
185
214
|
* @param {Object<string, {input?:number,output?:number,cache_read?:number,cache_creation?:number}>} modelUsage
|
|
@@ -199,7 +228,7 @@ function componentsFromModelUsage(modelUsage) {
|
|
|
199
228
|
const usage = u || {};
|
|
200
229
|
const fam = familyFor(rawId);
|
|
201
230
|
if (fam === null) unknown.push(canonicalizeModelId(rawId));
|
|
202
|
-
const p = PRICES[fam || 'opus']; //
|
|
231
|
+
const p = PRICES[fam || 'opus']; // mid-range fallback; unknown ids ride the `unknown` list
|
|
203
232
|
fresh_input_usd += ((Number(usage.input) || 0) * p.input) / 1e6;
|
|
204
233
|
output_usd += ((Number(usage.output) || 0) * p.output) / 1e6;
|
|
205
234
|
cache_read_usd += ((Number(usage.cache_read) || 0) * p.cache_read) / 1e6;
|
package/src/module-api.js
CHANGED
|
@@ -779,8 +779,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
779
779
|
// landed since 1.19.174 with no explicit bump. run 31970199477. (task 1002620)
|
|
780
780
|
// 1.19.176 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
781
781
|
// landed since 1.19.175 with no explicit bump. run 31971125373. (task 1002620)
|
|
782
|
+
// 1.19.177 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
783
|
+
// landed since 1.19.176 with no explicit bump. run 31971532260. (task 1002620)
|
|
784
|
+
// 1.19.178 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
785
|
+
// landed since 1.19.177 with no explicit bump. run 31971769062. (task 1002620)
|
|
782
786
|
// ---------------------------------------------------------------------------
|
|
783
|
-
const CORE_VERSION = '1.19.
|
|
787
|
+
const CORE_VERSION = '1.19.178'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
784
788
|
|
|
785
789
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
786
790
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/cost_reconcile.mjs
CHANGED
|
@@ -100,3 +100,33 @@ test('1366 missing per-tier fields default to 0 (partial model_usage)', () => {
|
|
|
100
100
|
assert.equal(out.cache_write_usd, 0);
|
|
101
101
|
assert.ok(near(out.total_usd, 5));
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
// ---------- fable / mythos family (task 1002883) ----------
|
|
105
|
+
// The reconciler is the SECOND pricing path the missing fable family broke: a
|
|
106
|
+
// cloud-box / overnight Fable session whose client cost hook never posted lands
|
|
107
|
+
// here, and before the fix it re-priced at the opus fallback — half the true
|
|
108
|
+
// cost, and flagged unknown on the platform's own default model.
|
|
109
|
+
|
|
110
|
+
test('1002883 fable 1M/1M/10M/1M → $10 / $50 / $10 / $12.50, total $82.50, not flagged', () => {
|
|
111
|
+
const out = componentsFromModelUsage({
|
|
112
|
+
'claude-fable-5': { input: M, output: M, cache_read: 10 * M, cache_creation: M },
|
|
113
|
+
});
|
|
114
|
+
assert.ok(near(out.fresh_input_usd, 10), `fresh ${out.fresh_input_usd}`);
|
|
115
|
+
assert.ok(near(out.output_usd, 50), `output ${out.output_usd}`);
|
|
116
|
+
assert.ok(near(out.cache_read_usd, 10), `cache_read ${out.cache_read_usd}`); // 10M * 1/M
|
|
117
|
+
assert.ok(near(out.cache_write_usd, 12.5), `cache_write ${out.cache_write_usd}`);
|
|
118
|
+
assert.ok(near(out.total_usd, 82.5), `total ${out.total_usd}`);
|
|
119
|
+
const sum = out.fresh_input_usd + out.cache_read_usd + out.cache_write_usd + out.output_usd;
|
|
120
|
+
assert.ok(near(sum, out.total_usd), `tiers sum ${sum} must equal total ${out.total_usd}`);
|
|
121
|
+
assert.deepEqual(out.unknown_models, [], 'fable is the platform default — never "unknown"');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('1002883 mythos prices as fable, and a fable session is 2× the same opus session', () => {
|
|
125
|
+
const usage = { input: M, output: M };
|
|
126
|
+
const fable = componentsFromModelUsage({ 'claude-fable-5': usage });
|
|
127
|
+
const mythos = componentsFromModelUsage({ 'claude-mythos-5': usage });
|
|
128
|
+
const opus = componentsFromModelUsage({ 'claude-opus-5': usage });
|
|
129
|
+
assert.ok(near(fable.total_usd, mythos.total_usd), 'mythos shares fable rates');
|
|
130
|
+
assert.deepEqual(mythos.unknown_models, []);
|
|
131
|
+
assert.ok(near(fable.total_usd, opus.total_usd * 2), `fable ${fable.total_usd} != 2× opus ${opus.total_usd}`);
|
|
132
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// tests/idea_full_pass_credit.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002996 (BV1.R172 / IC2, goal 1000067) — stream 2 of the ideator credit
|
|
4
|
+
// lane: a Full Idea with a passing completeness score pays its author 5 credits,
|
|
5
|
+
// once. ADR 0172.
|
|
6
|
+
//
|
|
7
|
+
// The done-when as assertions: a passing Full Idea books exactly 5; a thin /
|
|
8
|
+
// null-scored one books 0; re-running never double-pays; cost-plus-only does NOT
|
|
9
|
+
// suppress it (it gates on ideatorCredit, never perTaskCredit); an instance that
|
|
10
|
+
// opts the ideator lane out pays nothing.
|
|
11
|
+
//
|
|
12
|
+
// The credit fn (awardIdeaFullPassCredit) is exercised over a stub client that
|
|
13
|
+
// records statements and simulates the WHERE-NOT-EXISTS idempotency, so the
|
|
14
|
+
// idempotency logic is proven without a DB. Pure module import.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/idea_full_pass_credit.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const credits = require('../modules/economy/credits.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
async function test(name, fn) {
|
|
27
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A stub client whose credit_log "table" is an in-memory set of reasons, so the
|
|
32
|
+
// INSERT ... WHERE NOT EXISTS (SELECT 1 ... WHERE reason=$3) behaves exactly like
|
|
33
|
+
// Postgres: the second insert of the same reason is a no-op (rowCount 0).
|
|
34
|
+
function stubClient() {
|
|
35
|
+
const rows = [];
|
|
36
|
+
return {
|
|
37
|
+
rows,
|
|
38
|
+
async query(sql, params) {
|
|
39
|
+
// our fn issues exactly one INSERT ... SELECT ... WHERE NOT EXISTS, whose
|
|
40
|
+
// subquery filters on builder_id AND reason (the index-friendly predicate).
|
|
41
|
+
// Mirror both terms so the stub matches the real query.
|
|
42
|
+
const builderId = params[0];
|
|
43
|
+
const reason = params[2];
|
|
44
|
+
assert.match(sql, /builder_id = \$1 AND reason = \$3/, 'idempotency check must scope by builder_id to hit the index');
|
|
45
|
+
const exists = rows.some((r) => r.builder_id === builderId && r.reason === reason);
|
|
46
|
+
if (exists) return { rowCount: 0, rows: [] };
|
|
47
|
+
rows.push({ builder_id: builderId, delta: params[1], reason, description: params[3] });
|
|
48
|
+
return { rowCount: 1, rows: [] };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const PASS = 72; // clears the 60 bar
|
|
54
|
+
const THIN = 40; // below the bar
|
|
55
|
+
|
|
56
|
+
await test('a passing Full Idea books exactly 5 credits to its author', async () => {
|
|
57
|
+
const c = stubClient();
|
|
58
|
+
const r = await credits.awardIdeaFullPassCredit(c, { ideaId: 9, authorId: 3, completenessScore: PASS, config: { ideatorCredit: true } });
|
|
59
|
+
assert.equal(r.credited, credits.IDEA_FULL_PASS_CREDIT);
|
|
60
|
+
assert.equal(r.reason, 'paid');
|
|
61
|
+
assert.equal(c.rows.length, 1);
|
|
62
|
+
assert.equal(c.rows[0].delta, 5);
|
|
63
|
+
assert.equal(c.rows[0].builder_id, 3);
|
|
64
|
+
assert.equal(c.rows[0].reason, 'idea.credit.full_pass:9');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await test('re-running pays nothing — idempotent per idea', async () => {
|
|
68
|
+
const c = stubClient();
|
|
69
|
+
const opts = { ideaId: 9, authorId: 3, completenessScore: PASS, config: { ideatorCredit: true } };
|
|
70
|
+
const first = await credits.awardIdeaFullPassCredit(c, opts);
|
|
71
|
+
const second = await credits.awardIdeaFullPassCredit(c, opts);
|
|
72
|
+
const third = await credits.awardIdeaFullPassCredit(c, opts);
|
|
73
|
+
assert.equal(first.credited, 5);
|
|
74
|
+
assert.equal(second.credited, 0);
|
|
75
|
+
assert.equal(second.reason, 'already_paid');
|
|
76
|
+
assert.equal(third.credited, 0);
|
|
77
|
+
assert.equal(c.rows.length, 1, 'exactly one credit_log row ever, across three calls');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('a below-bar or null score books 0 — scoring gates the pay, not capture', async () => {
|
|
81
|
+
const c = stubClient();
|
|
82
|
+
assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 1, authorId: 3, completenessScore: THIN, config: { ideatorCredit: true } })).reason, 'below_bar');
|
|
83
|
+
assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 2, authorId: 3, completenessScore: null, config: { ideatorCredit: true } })).reason, 'below_bar');
|
|
84
|
+
assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 3, authorId: 3, completenessScore: undefined, config: { ideatorCredit: true } })).reason, 'below_bar');
|
|
85
|
+
assert.equal(c.rows.length, 0, 'nothing written for any sub-bar score');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await test('COST-PLUS-ONLY does NOT suppress it: the ideator lane pays regardless of perTaskCredit', async () => {
|
|
89
|
+
// the live cloudbongos.com policy: builder estimate off, ideator lane on
|
|
90
|
+
const policy = credits.rewardPolicy({ mode: 'cost-plus-only' });
|
|
91
|
+
assert.equal(policy.perTaskCredit, false);
|
|
92
|
+
assert.equal(policy.ideatorCredit, true);
|
|
93
|
+
const c = stubClient();
|
|
94
|
+
const r = await credits.awardIdeaFullPassCredit(c, { ideaId: 5, authorId: 3, completenessScore: PASS, config: policy });
|
|
95
|
+
assert.equal(r.credited, 5, 'paid despite cost-plus-only — this is the whole point of ADR 0172');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await test('an instance that opts the ideator lane out pays nothing', async () => {
|
|
99
|
+
const c = stubClient();
|
|
100
|
+
const r = await credits.awardIdeaFullPassCredit(c, { ideaId: 6, authorId: 3, completenessScore: PASS, config: { ideatorCredit: false } });
|
|
101
|
+
assert.equal(r.credited, 0);
|
|
102
|
+
assert.equal(r.reason, 'ideator_lane_off');
|
|
103
|
+
assert.equal(c.rows.length, 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await test('a missing author or idea is a safe 0, never a throw', async () => {
|
|
107
|
+
const c = stubClient();
|
|
108
|
+
assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 6, authorId: null, completenessScore: PASS, config: { ideatorCredit: true } })).reason, 'missing_author_or_idea');
|
|
109
|
+
assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: null, authorId: 3, completenessScore: PASS, config: { ideatorCredit: true } })).reason, 'missing_author_or_idea');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(`\nidea_full_pass_credit: ${passed} passed, ${failed} failed`);
|
|
113
|
+
if (failed > 0) process.exit(1);
|
package/tests/llm-pricing.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Prices VERIFIED 2026-06-20 against the live Anthropic models/pricing page
|
|
3
3
|
// (GDS-V4 task 1343): opus (4.8/4.7) $5/$25, sonnet (4.6) $3/$15, haiku (4.5)
|
|
4
4
|
// $1/$5; flat across the 1M window (no long-context premium).
|
|
5
|
+
// Re-VERIFIED + extended 2026-08-16 (task 1002883): fable (Fable 5 / Mythos 5)
|
|
6
|
+
// $10/$50 — the family familyFor() used to miss, so Fable sessions priced at the
|
|
7
|
+
// opus fallback and cost-plus under-paid by half.
|
|
5
8
|
// Run: node tests/llm-pricing.mjs
|
|
6
9
|
|
|
7
10
|
import { strict as assert } from 'node:assert';
|
|
@@ -18,6 +21,9 @@ const M = 1_000_000;
|
|
|
18
21
|
const near = (a, b) => Math.abs(a - b) < 1e-6;
|
|
19
22
|
|
|
20
23
|
test('familyFor matches by substring; unknown → null', () => {
|
|
24
|
+
assert.equal(familyFor('claude-fable-5'), 'fable');
|
|
25
|
+
assert.equal(familyFor('claude-mythos-5'), 'fable'); // same rates → same family
|
|
26
|
+
assert.equal(familyFor('claude-mythos-preview'), 'fable');
|
|
21
27
|
assert.equal(familyFor('claude-opus-4-8'), 'opus');
|
|
22
28
|
assert.equal(familyFor('claude-sonnet-4-6'), 'sonnet');
|
|
23
29
|
assert.equal(familyFor('claude-haiku-4-5'), 'haiku');
|
|
@@ -26,6 +32,68 @@ test('familyFor matches by substring; unknown → null', () => {
|
|
|
26
32
|
assert.equal(familyFor(undefined), null);
|
|
27
33
|
});
|
|
28
34
|
|
|
35
|
+
// ---------- fable / mythos family (task 1002883) ----------
|
|
36
|
+
// The regression this locks: claude-fable-5 is the owner's top-model default and
|
|
37
|
+
// the grader's assumed_builder_model, but familyFor() matched only opus/sonnet/
|
|
38
|
+
// haiku — so it fell to the opus fallback (unknown_model=true) at HALF its rate,
|
|
39
|
+
// and every cost-plus reward from a Fable session under-paid the builder.
|
|
40
|
+
|
|
41
|
+
test('fable 5: 1M input + 1M output = $10 + $50 = $60, and is NOT flagged unknown', () => {
|
|
42
|
+
const r = priceUsage('claude-fable-5', { input_tokens: M, output_tokens: M });
|
|
43
|
+
assert.ok(near(r.usd, 60), `got ${r.usd}`);
|
|
44
|
+
assert.equal(r.unknown_model, false, 'fable must resolve, not ride the opus fallback');
|
|
45
|
+
assert.equal(r.family, 'fable');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('mythos 5 prices identically to fable 5 (one family, shared rates)', () => {
|
|
49
|
+
const f = priceUsage('claude-fable-5', { input_tokens: M, output_tokens: M });
|
|
50
|
+
const m = priceUsage('claude-mythos-5', { input_tokens: M, output_tokens: M });
|
|
51
|
+
assert.ok(near(f.usd, m.usd), `fable ${f.usd} != mythos ${m.usd}`);
|
|
52
|
+
assert.equal(m.family, 'fable');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('fable cache tiers: read $1, 5m write $12.50, 1h write $20 per 1M', () => {
|
|
56
|
+
assert.ok(near(priceUsage('claude-fable-5', { cache_read_input_tokens: M }).usd, 1));
|
|
57
|
+
assert.ok(near(priceUsage('claude-fable-5', {
|
|
58
|
+
cache_creation_input_tokens: M,
|
|
59
|
+
cache_creation: { ephemeral_5m_input_tokens: M, ephemeral_1h_input_tokens: 0 },
|
|
60
|
+
}).usd, 12.5));
|
|
61
|
+
assert.ok(near(priceUsage('claude-fable-5', {
|
|
62
|
+
cache_creation_input_tokens: M,
|
|
63
|
+
cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: M },
|
|
64
|
+
}).usd, 20));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('fable rides the published cache multipliers off its own base input (1.25× / 2× / 0.1×)', () => {
|
|
68
|
+
const { fable } = PRICES;
|
|
69
|
+
assert.ok(near(fable.cache_write_5m, fable.input * 1.25));
|
|
70
|
+
assert.ok(near(fable.cache_write_1h, fable.input * 2));
|
|
71
|
+
assert.ok(near(fable.cache_read, fable.input * 0.1));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('fable is exactly 2× opus — the size of the pre-fix cost-plus shortfall', () => {
|
|
75
|
+
const usage = { input_tokens: M, output_tokens: M };
|
|
76
|
+
const fable = priceUsage('claude-fable-5', usage).usd;
|
|
77
|
+
const opus = priceUsage('claude-opus-5', usage).usd;
|
|
78
|
+
assert.ok(near(fable, opus * 2), `fable ${fable} != 2× opus ${opus}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('priceModelUsage: a fable session costs $60 / 1M+1M and reports no unknown models', () => {
|
|
82
|
+
const r = priceModelUsage({ 'claude-fable-5': { input: M, output: M } });
|
|
83
|
+
assert.ok(near(r.true_cost_usd, 60), `got ${r.true_cost_usd}`);
|
|
84
|
+
assert.deepEqual(r.unknown_models, [], 'fable must not be reported as unpriced');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('canonicalizeModelId collapses fable variants without changing the price', () => {
|
|
88
|
+
assert.equal(canonicalizeModelId('claude-fable-5[1m]'), 'claude-fable-5');
|
|
89
|
+
const r = priceModelUsage({
|
|
90
|
+
'claude-fable-5': { input: M },
|
|
91
|
+
'claude-fable-5[1m]': { input: M },
|
|
92
|
+
});
|
|
93
|
+
assert.equal(r.per_model.length, 2, 'raw keys kept as separate breakdown rows');
|
|
94
|
+
assert.ok(near(r.true_cost_usd, 20), `got ${r.true_cost_usd}`); // 10 + 10
|
|
95
|
+
});
|
|
96
|
+
|
|
29
97
|
test('opus 4.8: 1M input + 1M output = $5 + $25 = $30', () => {
|
|
30
98
|
const r = priceUsage('claude-opus-4-8', { input_tokens: M, output_tokens: M });
|
|
31
99
|
assert.ok(near(r.usd, 30), `got ${r.usd}`);
|