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
+108
View File
@@ -19,6 +19,114 @@ final class ToolModels
public const TIMELINE_STANDARD_MAX_CHARS = 300000;
public const TIMELINE_DEEP_MAX_CHARS = 600000;
/**
* Canonical redact engine registry. Server-side source of truth for which engines
* exist, their credit cost, and whether the client UI may offer them. Credits are
* ALWAYS resolved from here — never from a client-supplied value.
*
* gpu_legal is the fine-tuned legal model (Phase 6 roadmap) — wired but not yet
* client-selectable until the harness proves a lift.
*/
public const REDACT_ENGINES = [
'azure_mini' => ['label' => 'Azure GPT-4o-mini', 'credits' => 1, 'client_selectable' => true, 'speed' => 'fast'],
'azure_full' => ['label' => 'Azure GPT-4o', 'credits' => 2, 'client_selectable' => true, 'speed' => 'medium'],
'claude_haiku' => ['label' => 'Claude Haiku 4.5', 'credits' => 1, 'client_selectable' => true, 'speed' => 'fast'],
'claude_sonnet' => ['label' => 'Claude Sonnet 4.6', 'credits' => 2, 'client_selectable' => true, 'speed' => 'medium'],
'gpu' => ['label' => 'GPU Qwen 2.5 14B', 'credits' => 1, 'client_selectable' => true, 'speed' => 'slow'],
'gpu_legal' => ['label' => 'GPU Legal (Qwen FT)','credits' => 1, 'client_selectable' => false, 'speed' => 'slow'],
'regex' => ['label' => 'Regex only', 'credits' => 0, 'client_selectable' => false, 'speed' => 'instant'],
];
public static function isValidRedactEngine(string $engine): bool
{
return isset(self::REDACT_ENGINES[$engine]);
}
/** Normalise an incoming engine to a valid key, defaulting to claude_haiku. */
public static function redactEngine(string $engine): string
{
return self::isValidRedactEngine($engine) ? $engine : 'claude_haiku';
}
/** Server-side credit cost for a redact engine (never trust the client). */
public static function redactCredits(string $engine): int
{
return self::REDACT_ENGINES[self::redactEngine($engine)]['credits'];
}
/** Engine keys the client UI is allowed to offer. */
public static function redactSelectableEngines(): array
{
return array_keys(array_filter(
self::REDACT_ENGINES,
static fn(array $e) => $e['client_selectable']
));
}
/**
* Unified quality-tier registry for the Quick/Pro selector exposed on the analytical
* tools (summarize, ask, legal-analysis, barnevernet, korrespond, discrepancy,
* deep-research). Each tier maps to an existing engine string that the service layer
* already resolves to a gateway (resolveChatGateway/personaGateway) — so no service
* change is needed. Credits = the tool's base cost × tier multiplier, resolved
* server-side ONLY.
*
* Anchors picked from the 2026-06-15 tier benchmark (credits-first, Bedrock):
* quick → claude_haiku (96.7% quality, ~3.8s); pro → claude_sonnet (100%, ~13s).
*/
public const QUALITY_TIERS = [
'quick' => ['label' => 'Quick', 'engine' => 'claude_haiku', 'credit_mult' => 1],
'pro' => ['label' => 'Pro', 'engine' => 'claude_sonnet', 'credit_mult' => 2],
];
/** Normalise an incoming tier to a valid key, defaulting to quick. */
public static function qualityTier(string $tier): string
{
return isset(self::QUALITY_TIERS[$tier]) ? $tier : 'quick';
}
/** The existing engine string backing a quality tier. */
public static function tierEngine(string $tier): string
{
return self::QUALITY_TIERS[self::qualityTier($tier)]['engine'];
}
/** Human label for a quality tier (UI / telemetry). */
public static function tierLabel(string $tier): string
{
return self::QUALITY_TIERS[self::qualityTier($tier)]['label'];
}
/** Server-side credit cost for a tool at a given tier (base cost × tier multiplier). */
public static function tierCredits(string $tool, string $tier): int
{
$mult = self::QUALITY_TIERS[self::qualityTier($tier)]['credit_mult'];
return max(1, PricingCatalog::toolCost($tool) * $mult);
}
/**
* Resolve a requested quality tier for a user + tool. Applies the subscription gate
* (free / anonymous → quick only; plus & pro may pick pro) and returns the backing
* engine plus the server-side credit cost. Credits are NEVER trusted from the client.
*
* @return array{tier:string,engine:string,credits:int,label:string}
*/
public static function resolveTier(int $userId, string $tool, string $requestedTier): array
{
$tier = self::qualityTier($requestedTier);
if ($tier === 'pro' && $userId > 0 && FreeTier::tier($userId) === 'free') {
$tier = 'quick';
}
return [
'tier' => $tier,
'engine' => self::tierEngine($tier),
'credits' => self::tierCredits($tool, $tier),
'label' => self::tierLabel($tier),
];
}
public static function engineForUser(int $userId, string $requestedEngine): string
{
$valid = ['nova_lite', 'azure_mini', 'azure_full', 'gpu', 'regex', 'claude_haiku', 'claude_sonnet'];