feat(tools): DB-backed LLM engine admin (owner-only)

Add an owner-gated dashboard to remap any tool/tier's model live without a
code push. Overrides live in dbn_tool_engine_config (dobetternorge_maindb)
and are consulted by dbnToolsResolveToolRun() + dbnToolsReviewerModel();
no row = unchanged code/.env behaviour (fully back-compatible).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:55:17 +02:00
parent f270a32056
commit 2f05b84b0f
7 changed files with 546 additions and 2 deletions
+78 -2
View File
@@ -180,6 +180,64 @@ function dbnToolsAuthenticatedUser(): ?array
];
}
/**
* Operator gate for the LLM-engine admin. True when the authenticated session is the
* tenant owner, or when their email is listed in the DBN_ADMIN_EMAILS allowlist (so an
* SSO operator can self-authorise without an 'owner' client_users row).
*/
function dbnToolsIsOwner(): bool
{
if (!dbnToolsIsAuthenticated()) {
return false;
}
if (strtolower((string)($_SESSION['dbn_tools_user_role'] ?? '')) === 'owner') {
return true;
}
$email = strtolower(trim((string)($_SESSION['dbn_tools_user_email'] ?? $_SESSION['dbn_tools_sso_email'] ?? '')));
if ($email === '') {
return false;
}
$allow = array_filter(array_map('trim', explode(',', strtolower((string)(dbnToolsEnv('DBN_ADMIN_EMAILS', '') ?? '')))));
return in_array($email, $allow, true);
}
/**
* Look up an operator engine override for a tool + scope from dbn_tool_engine_config
* (dobetternorge_maindb). Prefers a tool-specific row, then a '*' all-tools row.
* Returns null when no enabled override exists. Statically cached per request; safe
* (returns null) when the table is absent, so behaviour is unchanged pre-migration.
*
* scope: tier_quick | tier_pro | legacy | persona_family | persona_general
*/
function dbnToolsEngineOverride(string $tool, string $scope): ?string
{
static $cache = null;
if ($cache === null) {
$cache = [];
try {
$rows = dbnmDb()->query('SELECT tool_slug, scope, engine FROM dbn_tool_engine_config WHERE enabled = 1')->fetchAll();
foreach ($rows as $r) {
$cache[$r['tool_slug'] . '|' . $r['scope']] = (string)$r['engine'];
}
} catch (Throwable $e) {
$cache = []; // table missing / DB down → no overrides
}
}
return $cache[$tool . '|' . $scope] ?? $cache['*|' . $scope] ?? null;
}
/** Reviewer/persona model ids an operator override may select (validated against this). */
function dbnToolsReviewerModelKeys(): array
{
return ['gpt-4o', 'gpt-4o-mini', 'dbn-legal-agent-v3', 'dbn-legal-agent', 'dobetter-norge-v4', 'qwen2.5:14b'];
}
/** True if $model is an accepted reviewer/persona model id. */
function dbnToolsIsValidReviewerModel(string $model): bool
{
return in_array($model, dbnToolsReviewerModelKeys(), true);
}
function dbnToolsRequiredPackageSlug(): string
{
return dbnToolsEnv('DBN_CAVEAU_PACKAGE_SLUG') ?: 'family-legal';
@@ -506,9 +564,17 @@ function dbnToolsPersonaTrack(?string $slug): string
function dbnToolsReviewerModel(?string $slug = null): string
{
if (dbnToolsPersonaTrack($slug) === 'family') {
$override = dbnToolsEngineOverride('*', 'persona_family');
if ($override !== null && dbnToolsIsValidReviewerModel($override)) {
return $override;
}
return trim((string)(dbnToolsEnv('DBN_REVIEW_MODEL_FAMILY', 'dbn-legal-agent-v3') ?? 'dbn-legal-agent-v3'))
?: 'dbn-legal-agent-v3';
}
$override = dbnToolsEngineOverride('*', 'persona_general');
if ($override !== null && dbnToolsIsValidReviewerModel($override)) {
return $override;
}
return trim((string)(dbnToolsEnv('DBN_REVIEW_MODEL_GENERAL', 'gpt-4o') ?? 'gpt-4o')) ?: 'gpt-4o';
}
@@ -790,6 +856,11 @@ function dbnToolsResolveToolRun(string $tool, array $input, string $legacyDefaul
{
if (isset($input['tier'])) {
$res = ToolModels::resolveTier(dbnToolsFreeTierUid(), $tool, (string)$input['tier']);
$scope = $res['tier'] === 'pro' ? 'tier_pro' : 'tier_quick';
$override = dbnToolsEngineOverride($tool, $scope);
if ($override !== null && ToolModels::isValidTierEngine($override)) {
$res['engine'] = $override; // credits stay tied to the tier; only the engine swaps
}
$ftUid = dbnToolsFreeTierCheckAmount($tool, $res['credits']);
return [
'tier' => $res['tier'],
@@ -800,8 +871,13 @@ function dbnToolsResolveToolRun(string $tool, array $input, string $legacyDefaul
];
}
$ftUid = dbnToolsFreeTierCheck($tool);
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? $legacyDefaultEngine));
$ftUid = dbnToolsFreeTierCheck($tool);
$requested = (string)($input['engine'] ?? $legacyDefaultEngine);
$override = dbnToolsEngineOverride($tool, 'legacy');
if ($override !== null && ToolModels::isValidTierEngine($override)) {
$requested = $override; // still passes through engineForUser tier gating below
}
$engine = ToolModels::engineForUser($ftUid, $requested);
return [
'tier' => '',
'engine' => $engine,