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:
+78
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user