diff --git a/advocate.php b/advocate.php index a73a7a0..10dcb6e 100644 --- a/advocate.php +++ b/advocate.php @@ -50,12 +50,12 @@ require_once __DIR__ . '/includes/layout.php'; 0 / 4,000 -
Both engines run on AWS Bedrock via Claude. Most of the time is spent on multiple question-answering passes — 6–10 sub-questions each requiring a full retrieval and answer cycle. Haiku is faster and handles most cases well. Sonnet produces a more thorough brief with deeper ECHR precedent analysis and stronger multi-party argumentation.
+Most of the time is spent on multiple question-answering passes — 6–10 sub-questions each requiring a full retrieval and answer cycle. Quick uses Claude Haiku 4.5 — faster and handles most cases well (3 credits). Pro uses Claude Sonnet 4.6 — a more thorough brief with deeper ECHR precedent analysis and stronger multi-party argumentation (6 credits).
Corpus slices
diff --git a/api/ask.php b/api/ask.php index b902a95..1cee7f5 100644 --- a/api/ask.php +++ b/api/ask.php @@ -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']); diff --git a/api/barnevernet.php b/api/barnevernet.php index 6677966..bcd8f12 100644 --- a/api/barnevernet.php +++ b/api/barnevernet.php @@ -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; } diff --git a/api/deep-research.php b/api/deep-research.php index 0dfee86..212b59d 100644 --- a/api/deep-research.php +++ b/api/deep-research.php @@ -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; } diff --git a/api/discrepancy.php b/api/discrepancy.php index 3e182fb..a108f5c 100644 --- a/api/discrepancy.php +++ b/api/discrepancy.php @@ -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; } diff --git a/api/korrespond.php b/api/korrespond.php index a249c84..7643422 100644 --- a/api/korrespond.php +++ b/api/korrespond.php @@ -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 */ } diff --git a/api/summarize.php b/api/summarize.php index 2e6de66..93f7c75 100644 --- a/api/summarize.php +++ b/api/summarize.php @@ -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; } diff --git a/assets/js/advocate.js b/assets/js/advocate.js index 22048f4..7078cc8 100644 --- a/assets/js/advocate.js +++ b/assets/js/advocate.js @@ -64,7 +64,7 @@ roleCustom: document.getElementById('advRoleCustom'), slices: Array.from(document.querySelectorAll('.adv-slice')), langButtons: Array.from(document.querySelectorAll('#advLangSwitcher .lang-btn')), - engineRadios: Array.from(document.querySelectorAll('input[name="advEngine"]')), + tierRadios: Array.from(document.querySelectorAll('input[name="advTier"]')), subQ: document.getElementById('advSubQ'), subQVal: document.getElementById('advSubQValue'), chunkLimit: document.getElementById('advChunkLimit'), @@ -337,9 +337,9 @@ return out; } - function getEngine() { - const checked = els.engineRadios.find((r) => r.checked); - return checked ? checked.value : 'azure_mini'; + function getTier() { + const checked = els.tierRadios.find((r) => r.checked); + return checked ? checked.value : 'quick'; } function getControls() { @@ -371,10 +371,10 @@ return; } - const engine = getEngine(); - const expectedDuration = engine === 'azure_full' - ? '60–180 seconds with Azure gpt-4o' - : (engine === 'gpu' ? '30–90 seconds on GPU' : '15–45 seconds with Azure gpt-4o-mini'); + const tier = getTier(); + const expectedDuration = tier === 'pro' + ? '3–5 minutes with Claude Sonnet' + : '2–4 minutes with Claude Haiku'; setStatus(`Building advocate brief for ${advocateRole}… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; @@ -387,7 +387,7 @@ query, paste_text: '', slices, - engine, + tier, language: lang, controls: getControls(), advocate_role: advocateRole, @@ -505,7 +505,7 @@ els.runButton.disabled = false; renderTrace(finalResult.trace || []); renderResults(finalResult); - saveToCache(finalResult, { query, role: advocateRole, engine, slices, lang }); + saveToCache(finalResult, { query, role: advocateRole, tier, slices, lang }); function handleStreamEvent(evt) { if (!evt || !evt.event) return; @@ -957,7 +957,7 @@ els.input.value = formState.query || ''; updateCharCount(); if (formState.role) els.roleSelect.value = formState.role; - const radio = els.engineRadios.find((r) => r.value === formState.engine); + const radio = els.tierRadios.find((r) => r.value === formState.tier); if (radio) radio.checked = true; if (formState.slices) { els.slices.forEach((btn) => { @@ -1074,7 +1074,7 @@ body: JSON.stringify({ query, language: lang, - engine: getEngine(), + engine: getTier() === 'pro' ? 'claude_sonnet' : 'claude_haiku', controls: getControls(), advocate_role: advocateRole, }), diff --git a/assets/js/barnevernet.js b/assets/js/barnevernet.js index 1331e2a..4b23637 100644 --- a/assets/js/barnevernet.js +++ b/assets/js/barnevernet.js @@ -55,7 +55,7 @@ roleCustom: document.getElementById('bvjRoleCustom'), slices: Array.from(document.querySelectorAll('.adv-slice')), langButtons: Array.from(document.querySelectorAll('#bvjLangSwitcher .lang-btn')), - engineRadios: Array.from(document.querySelectorAll('input[name="bvjEngine"]')), + tierRadios: Array.from(document.querySelectorAll('input[name="bvjTier"]')), subQ: document.getElementById('bvjSubQ'), subQVal: document.getElementById('bvjSubQValue'), chunkLimit: document.getElementById('bvjChunkLimit'), @@ -186,9 +186,9 @@ }; } - function getEngine() { - const checked = els.engineRadios.find((r) => r.checked); - return checked ? checked.value : 'azure_mini'; + function getTier() { + const checked = els.tierRadios.find((r) => r.checked); + return checked ? checked.value : 'quick'; } // ── File upload ──────────────────────────────────────────────────────────── @@ -385,13 +385,11 @@ return; } - const engine = getEngine(); + const tier = getTier(); const additionalNotes = (els.notes ? els.notes.value : '').trim(); - const expectedDuration = engine === 'azure_full' - ? '90–180 seconds with Azure gpt-4o' - : (engine === 'gpu' ? '45–90 seconds on GPU' - : (engine === 'dbn_legal' ? '60–120 seconds with Norwegian specialist' - : '30–60 seconds with Azure gpt-4o-mini')); + const expectedDuration = tier === 'pro' + ? '90–180 seconds with Claude Sonnet' + : '30–60 seconds with Claude Haiku'; setStatus(`Analysing document for ${advocateRole}… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; @@ -403,7 +401,7 @@ const payload = { advocate_role: advocateRole, - engine, + tier, language: lang, slices, controls: getControls(), diff --git a/assets/js/deep-research.js b/assets/js/deep-research.js index eb5a97b..b6aab20 100644 --- a/assets/js/deep-research.js +++ b/assets/js/deep-research.js @@ -42,7 +42,7 @@ traceList: document.getElementById('traceList'), slices: Array.from(document.querySelectorAll('.dr-slice')), langButtons: Array.from(document.querySelectorAll('#drLangSwitcher .lang-btn')), - engineRadios: Array.from(document.querySelectorAll('input[name="drEngine"]')), + tierRadios: Array.from(document.querySelectorAll('input[name="drTier"]')), personaControl: document.getElementById('drPersonaControl'), personaSelect: document.getElementById('drPersonaSelect'), subQ: document.getElementById('drSubQ'), @@ -280,9 +280,9 @@ return out; } - function getEngine() { - const checked = els.engineRadios.find((r) => r.checked); - return checked ? checked.value : 'azure_mini'; + function getTier() { + const checked = els.tierRadios.find((r) => r.checked); + return checked ? checked.value : 'quick'; } function getControls() { @@ -308,10 +308,10 @@ return; } - const engine = getEngine(); - const expectedDuration = engine === 'azure_full' - ? '60–180 seconds with Azure gpt-4o' - : (engine === 'gpu' ? '30–90 seconds on GPU' : '15–45 seconds with Azure gpt-4o-mini'); + const tier = getTier(); + const expectedDuration = tier === 'pro' + ? '60–180 seconds with Claude Sonnet' + : '15–45 seconds with Claude Haiku'; setStatus(`Running deep research… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; @@ -325,7 +325,7 @@ query, paste_text: '', slices, - engine, + tier, language: lang, controls: getControls(), }; diff --git a/assets/js/discrepancy.js b/assets/js/discrepancy.js index e77a8d6..7fb1841 100644 --- a/assets/js/discrepancy.js +++ b/assets/js/discrepancy.js @@ -49,7 +49,7 @@ results: document.getElementById('dcResults'), traceList: document.getElementById('traceList'), langButtons: Array.from(document.querySelectorAll('#dcLangSwitcher .lang-btn')), - engineRadios: Array.from(document.querySelectorAll('input[name="dcEngine"]')), + tierRadios: Array.from(document.querySelectorAll('input[name="dcTier"]')), slices: Array.from(document.querySelectorAll('.adv-slice')), // File A zoneA: document.getElementById('dcZoneA'), @@ -188,12 +188,10 @@ return; } - const engine = (els.engineRadios.find((r) => r.checked) || {}).value || 'azure_mini'; + const tier = (els.tierRadios.find((r) => r.checked) || {}).value || 'quick'; const slices = getSelectedSlices(); - const expectedDuration = engine === 'azure_full' ? '2-3 minutes' - : engine === 'gpu' ? '~90 seconds' - : '60-90 seconds'; + const expectedDuration = tier === 'pro' ? '2-3 minutes' : '60-90 seconds'; setStatus(`Comparing documents… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; @@ -203,7 +201,7 @@ renderTrace(stepState); const payload = { - engine, language: lang, slices, + tier, language: lang, slices, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, }; const form = new FormData(); diff --git a/assets/js/korrespond.js b/assets/js/korrespond.js index 932af30..45a9664 100644 --- a/assets/js/korrespond.js +++ b/assets/js/korrespond.js @@ -280,7 +280,7 @@ clarifications: pendingClarifications, force_draft: !!forceDraft, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, - engine: (document.querySelector('[name="korrEngine"]:checked')?.value ?? 'azure_mini'), + tier: (document.querySelector('[name="korrTier"]:checked')?.value ?? 'quick'), length: (document.querySelector('[name="korrLength"]:checked')?.value ?? 'standard'), }; if (korrDocIds.length) payload.doc_ids = korrDocIds; diff --git a/assets/js/summarize.js b/assets/js/summarize.js index 3f69455..e744e92 100644 --- a/assets/js/summarize.js +++ b/assets/js/summarize.js @@ -179,13 +179,13 @@ return; } - var engine = (document.querySelector('input[name="sumEngine"]:checked') || {}).value || 'azure_mini'; + var tier = (document.querySelector('input[name="sumTier"]:checked') || {}).value || 'quick'; var slices = activeSlices(); var payload = { text: combined, language: _currentLang, - engine: engine, + tier: tier, depth: (document.querySelector('input[name="sumDepth"]:checked') || {}).value || 'standard', slices: slices, }; diff --git a/barnevernet.php b/barnevernet.php index a1a0988..60232b6 100644 --- a/barnevernet.php +++ b/barnevernet.php @@ -36,14 +36,12 @@ require_once __DIR__ . '/includes/layout.php';The agent will analyse the document from your perspective — identifying supporting statutes, procedural red flags, and ECHR arguments for your position.
Engine applies to the final advocacy synthesis only. Norwegian specialist v3 is the recommended choice for Barnevernet documents — it is fine-tuned on § 4-25, Strand Lobben, forvaltningsloven § 17/§ 41, and procedural red-flag detection. Classification, party extraction, and timeline always use azure-mini.
+Quality applies to the final advocacy synthesis only. Quick uses Claude Haiku 4.5 — fast and accurate for most Barnevernet documents (3 credits). Pro uses Claude Sonnet 4.6 — better at § 4-25 threshold errors, Strand Lobben / forvaltningsloven § 17/§ 41 procedural red flags, and ECHR argumentation (6 credits). Classification, party extraction, and timeline always use the quick engine.
Corpus slices
diff --git a/deep-research.php b/deep-research.php index 3de94ba..decd5b6 100644 --- a/deep-research.php +++ b/deep-research.php @@ -16,14 +16,12 @@ require_once __DIR__ . '/includes/layout.php';Azure mini is the default and finishes fastest. Azure full is the most thorough. Norwegian specialist v3 is a Qwen2.5 fine-tune optimised for barnevernsloven, ECHR, and forvaltningsloven — best for cases involving § 4-25, Strand Lobben, or procedural challenges.
+Quick uses Claude Haiku 4.5 — fast and accurate for most research (6 credits). Pro uses Claude Sonnet 4.6 — the most thorough synthesis, best for cases involving § 4-25, Strand Lobben, or procedural challenges (12 credits).
Engine applies to the final synthesis only. Norwegian specialist v3 excels at identifying legally significant discrepancies in Barnevernet documents — procedural violations, threshold errors, and missing statutory justifications. Classification, party extraction, timelines, and cross-referencing always use azure-mini.
+Quick uses Claude Haiku 4.5 — fast and accurate for most document comparisons (4 credits). Pro uses Claude Sonnet 4.6 — better at legally significant discrepancies in Barnevernet documents, procedural violations, and threshold errors (8 credits).
Concise: short and to-the-point (2-3 paragraphs). Standard: balanced correspondence. Detailed: full background, all arguments, and complete legal reasoning.
- -Quick uses Claude Haiku 4.5 for drafting — fast and solid for standard correspondence. Thorough uses Claude Sonnet 4.6 — better at multi-statute cases, complex appeal grounds, and ECHR framing.
+Quick uses Claude Haiku 4.5 for drafting — fast and solid for standard correspondence (3 credits). Pro uses Claude Sonnet 4.6 — better at multi-statute cases, complex appeal grounds, and ECHR framing (6 credits).
Standard uses Claude Haiku 4.5 — fast and highly accurate. Deep uses Claude Sonnet 4.6 — best for complex multi-statute documents.
+Quick uses Claude Haiku 4.5 — fast and highly accurate (1 credit). Pro uses Claude Sonnet 4.6 — best for complex multi-statute documents (2 credits).