feat(tools): converge two-tier Quick/Pro selector onto .no fork

Port the dobetterlegal-tools two-tier quality stack to dobetternorge.no:
QUALITY_TIERS registry + resolveTier (ToolModels), dbnToolsResolveToolRun
(bootstrap), tier read+charge in the 6 analytical endpoints, Quick/Pro
UI + payload.tier on the 6 tool pages/JS, and the bounded
corpusContextForSummarize RAG fix (per-passage trim + total budget +
reranker_enabled). Back-compat: requests without `tier` keep legacy
engine behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:23:46 +02:00
parent b217f18118
commit a8b1bb87a6
21 changed files with 339 additions and 103 deletions
+4 -3
View File
@@ -6,10 +6,11 @@ require_once __DIR__ . '/../includes/ToolModels.php';
dbnToolsRequireMethod('POST');
dbnToolsRequireAuth();
$ftUid = dbnToolsFreeTierCheck('ask');
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
$input = dbnToolsJsonInput(25000);
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
$run = dbnToolsResolveToolRun('ask', $input);
$ftUid = $run['ftUid'];
$engine = $run['engine'];
dbnToolsWithChargedTelemetry('ask', $language, $ftUid, function () use ($input, $language, $engine): array {
$question = dbnToolsInjectDocContent($input, dbnToolsString($input, 'question', 4000, false));
@@ -20,4 +21,4 @@ dbnToolsWithChargedTelemetry('ask', $language, $ftUid, function () use ($input,
? trim($input['profile'])
: null;
return (new DbnLegalToolsService())->ask($question, $language, $engine, $persona);
});
}, $run['credits'], $run['metadata']);
+20 -2
View File
@@ -53,7 +53,23 @@ try {
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
$advocateRole = trim((string)($input['advocate_role'] ?? ''));
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
if (isset($input['tier'])) {
$run = ToolModels::resolveTier(dbnToolsFreeTierUid(), 'barnevernet', (string)$input['tier']);
$engine = $run['engine'];
$tierCredits = $run['credits'];
$tierMeta = ['tier' => $run['tier'], 'engine' => $engine];
if ($ftUid > 0) {
$gate = FreeTier::checkAmount($ftUid, 'barnevernet', $tierCredits);
if (empty($gate['ok'])) {
$emit('error', ['code' => $gate['reason'] ?? 'no_credits', 'message' => 'Insufficient credits for the selected tier.']);
exit;
}
}
} else {
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
$tierCredits = null;
$tierMeta = [];
}
$sliceInput = $input['slices'] ?? [];
$controls = is_array($input['controls'] ?? null) ? $input['controls'] : [];
$additionalNotes = mb_substr(dbnToolsInjectDocContent($input, trim((string)($input['additional_notes'] ?? ''))), 0, 8000, 'UTF-8');
@@ -154,7 +170,9 @@ try {
'bvj_doc_type' => $result['doc_meta']['doc_type'] ?? null,
]);
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'barnevernet');
$ftRemaining = $tierCredits === null
? dbnToolsFreeTierDeduct($ftUid, 'barnevernet')
: dbnToolsFreeTierDeductAmount($ftUid, 'barnevernet', $tierCredits, $tierMeta);
if ($ftRemaining >= 0) {
$result['balance'] = $ftRemaining;
}
+6 -3
View File
@@ -65,8 +65,9 @@ try {
throw new DbnToolsHttpException('advocate_role is too long.', 422, 'advocate_role_too_long');
}
$chargeTool = $advocateRole !== '' ? 'advocate' : 'deep-research';
$ftUid = dbnToolsFreeTierCheck($chargeTool);
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
$run = dbnToolsResolveToolRun($chargeTool, $input);
$ftUid = $run['ftUid'];
$engine = $run['engine'];
$priorContext = is_array($input['prior_context'] ?? null) ? $input['prior_context'] : null;
$branchNotes = mb_substr(trim((string)($input['branch_notes'] ?? '')), 0, 1000, 'UTF-8');
$subQsOverride = is_array($input['sub_questions_override'] ?? null) ? $input['sub_questions_override'] : [];
@@ -160,7 +161,9 @@ try {
'advocate_role' => $advocateRole !== '' ? $advocateRole : null,
]);
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, $chargeTool);
$ftRemaining = $run['credits'] === null
? dbnToolsFreeTierDeduct($ftUid, $chargeTool)
: dbnToolsFreeTierDeductAmount($ftUid, $chargeTool, $run['credits'], $run['metadata']);
if ($ftRemaining >= 0) {
$result['balance'] = $ftRemaining;
}
+20 -2
View File
@@ -41,7 +41,23 @@ try {
}
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
if (isset($input['tier'])) {
$run = ToolModels::resolveTier(dbnToolsFreeTierUid(), 'discrepancy', (string)$input['tier']);
$engine = $run['engine'];
$tierCredits = $run['credits'];
$tierMeta = ['tier' => $run['tier'], 'engine' => $engine];
if ($ftUid > 0) {
$gate = FreeTier::checkAmount($ftUid, 'discrepancy', $tierCredits);
if (empty($gate['ok'])) {
$emit('error', ['code' => $gate['reason'] ?? 'no_credits', 'message' => 'Insufficient credits for the selected tier.']);
exit;
}
}
} else {
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
$tierCredits = null;
$tierMeta = [];
}
$sliceInput = $input['slices'] ?? [];
// Extract file A
@@ -144,7 +160,9 @@ try {
'deployment' => $result['trace_metadata']['deployment'] ?? null,
]);
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'discrepancy');
$ftRemaining = $tierCredits === null
? dbnToolsFreeTierDeduct($ftUid, 'discrepancy')
: dbnToolsFreeTierDeductAmount($ftUid, 'discrepancy', $tierCredits, $tierMeta);
if ($ftRemaining >= 0) {
$result['balance'] = $ftRemaining;
}
+16 -7
View File
@@ -165,15 +165,24 @@ try {
}
// ── Deduct credit now (Pass 2 starts) ───────────────────────────────────────
$ftUid = dbnToolsFreeTierCheck('korrespond');
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
$inputEngine = (string)($input['engine'] ?? '');
if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) {
$engine = $inputEngine;
if (isset($input['tier'])) {
$run = dbnToolsResolveToolRun('korrespond', $input);
$ftUid = $run['ftUid'];
$engine = $run['engine'];
} else {
$ftUid = dbnToolsFreeTierCheck('korrespond');
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
$inputEngine = (string)($input['engine'] ?? '');
if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) {
$engine = $inputEngine;
}
$run = ['credits' => null, 'metadata' => []];
}
$length = in_array($input['length'] ?? '', ['concise', 'standard', 'detailed'], true)
? (string)$input['length'] : 'standard';
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond');
$ftRemaining = $run['credits'] === null
? dbnToolsFreeTierDeduct($ftUid, 'korrespond')
: dbnToolsFreeTierDeductAmount($ftUid, 'korrespond', $run['credits'], $run['metadata']);
$creditDeducted = true;
$personaSlug = (isset($input['profile']) && is_string($input['profile']) && trim($input['profile']) !== '')
@@ -212,7 +221,7 @@ try {
'case_doc_ids' => $GLOBALS['dbn_last_case_doc_ids'] ?? [],
'model' => $engine,
'latency_ms' => $result['latency_ms'],
'credits_charged' => 1,
'credits_charged' => $run['credits'] ?? 1,
]);
} catch (Throwable) { /* non-critical */ }
+6 -3
View File
@@ -6,11 +6,12 @@ require_once __DIR__ . '/../includes/ToolModels.php';
dbnToolsRequireMethod('POST');
dbnToolsRequireAuth();
$ftUid = dbnToolsFreeTierCheck('summarize');
$input = dbnToolsJsonInput(400000);
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
$run = dbnToolsResolveToolRun('summarize', $input);
$ftUid = $run['ftUid'];
$engine = $run['engine'];
$depth = in_array($input['depth'] ?? '', ['brief', 'standard', 'detailed'], true)
? (string)$input['depth'] : 'standard';
$slices = is_array($input['slices'] ?? null) ? array_values(array_filter($input['slices'])) : [];
@@ -73,7 +74,9 @@ try {
$result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext, $depth);
if ($ftUid > 0) {
$balance = dbnToolsFreeTierDeduct($ftUid, 'summarize');
$balance = $run['credits'] === null
? dbnToolsFreeTierDeduct($ftUid, 'summarize')
: dbnToolsFreeTierDeductAmount($ftUid, 'summarize', $run['credits'], $run['metadata']);
$result['balance'] = $balance;
}