2f05b84b0f
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>
214 lines
7.6 KiB
PHP
214 lines
7.6 KiB
PHP
<?php
|
|
/**
|
|
* /api/dashboard/engines.php — owner-only LLM engine override admin.
|
|
*
|
|
* GET → { ok, context, tier_engines, reviewer_models, tier_tools, rows }
|
|
* POST ?action=set body: { tool_slug, scope, engine } → upsert an override
|
|
* POST ?action=clear body: { tool_slug, scope } → delete (revert to default)
|
|
*
|
|
* Overrides live in dobetternorge_maindb.dbn_tool_engine_config and are consulted by
|
|
* dbnToolsResolveToolRun() / dbnToolsReviewerModel(). Owner-gated via dbnToolsIsOwner().
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__, 2) . '/includes/bootstrap.php';
|
|
require_once dirname(__DIR__, 2) . '/includes/ToolModels.php';
|
|
|
|
dbnToolsRequireAuth();
|
|
|
|
if (!dbnToolsIsOwner()) {
|
|
dbnToolsError('Owner access required.', 403, 'forbidden');
|
|
}
|
|
|
|
const ENGINES_TIER_TOOLS = ['ask', 'summarize', 'deep-research', 'korrespond', 'barnevernet', 'discrepancy'];
|
|
const ENGINES_TIER_SCOPES = ['tier_quick', 'tier_pro', 'legacy'];
|
|
const ENGINES_PERSONA_SCOPES = ['persona_family', 'persona_general'];
|
|
|
|
function enginesTierLabel(string $key): string
|
|
{
|
|
return [
|
|
'claude_haiku' => 'Claude Haiku 4.5',
|
|
'claude_sonnet' => 'Claude Sonnet 4.6',
|
|
'azure_mini' => 'Azure GPT-4o-mini',
|
|
'azure_full' => 'Azure GPT-4o',
|
|
'gpu' => 'GPU Qwen 2.5 14B',
|
|
'nova_lite' => 'Nova Lite',
|
|
'regex' => 'Regex only',
|
|
][$key] ?? $key;
|
|
}
|
|
|
|
function enginesReviewerLabel(string $key): string
|
|
{
|
|
return [
|
|
'gpt-4o' => 'GPT-4o (Azure)',
|
|
'gpt-4o-mini' => 'GPT-4o-mini (Azure)',
|
|
'dbn-legal-agent-v3' => 'DBN Legal v3 (GPU fine-tune)',
|
|
'dbn-legal-agent' => 'DBN Legal (GPU)',
|
|
'dobetter-norge-v4' => 'Do Better Norge v4 (GPU)',
|
|
'qwen2.5:14b' => 'Qwen 2.5 14B',
|
|
][$key] ?? $key;
|
|
}
|
|
|
|
/** Code/.env default engine for a given scope (what applies when no override is set). */
|
|
function enginesCodeDefault(string $scope): string
|
|
{
|
|
switch ($scope) {
|
|
case 'tier_quick': return ToolModels::tierEngine('quick');
|
|
case 'tier_pro': return ToolModels::tierEngine('pro');
|
|
case 'legacy': return 'azure_mini';
|
|
case 'persona_family':
|
|
return trim((string)(dbnToolsEnv('DBN_REVIEW_MODEL_FAMILY', 'dbn-legal-agent-v3') ?? '')) ?: 'dbn-legal-agent-v3';
|
|
case 'persona_general':
|
|
return trim((string)(dbnToolsEnv('DBN_REVIEW_MODEL_GENERAL', 'gpt-4o') ?? '')) ?: 'gpt-4o';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function enginesValidScope(string $scope): bool
|
|
{
|
|
return in_array($scope, array_merge(ENGINES_TIER_SCOPES, ENGINES_PERSONA_SCOPES), true);
|
|
}
|
|
|
|
function enginesValidEngineForScope(string $scope, string $engine): bool
|
|
{
|
|
if (in_array($scope, ENGINES_PERSONA_SCOPES, true)) {
|
|
return dbnToolsIsValidReviewerModel($engine);
|
|
}
|
|
return ToolModels::isValidTierEngine($engine);
|
|
}
|
|
|
|
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
|
|
|
try {
|
|
if ($method === 'GET') {
|
|
enginesGet();
|
|
} else {
|
|
$action = (string)($_GET['action'] ?? '');
|
|
if ($action === 'set') {
|
|
enginesSet();
|
|
} elseif ($action === 'clear') {
|
|
enginesClear();
|
|
} else {
|
|
dbnToolsError('Unknown action.', 400, 'unknown_action');
|
|
}
|
|
}
|
|
} catch (DbnToolsHttpException $e) {
|
|
dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra ?? []);
|
|
} catch (Throwable $e) {
|
|
error_log('[dbn-engines] ' . $e->getMessage());
|
|
dbnToolsError('Engine admin operation failed.', 500, 'op_failed');
|
|
}
|
|
|
|
function enginesLoadOverrides(): array
|
|
{
|
|
$map = [];
|
|
try {
|
|
$rows = dbnmDb()->query('SELECT tool_slug, scope, engine, updated_by, updated_at FROM dbn_tool_engine_config WHERE enabled = 1')->fetchAll();
|
|
foreach ($rows as $r) {
|
|
$map[$r['tool_slug'] . '|' . $r['scope']] = $r;
|
|
}
|
|
} catch (Throwable $e) {
|
|
// table not yet migrated → no overrides
|
|
}
|
|
return $map;
|
|
}
|
|
|
|
function enginesGet(): void
|
|
{
|
|
$overrides = enginesLoadOverrides();
|
|
|
|
$tierEngines = array_map(
|
|
static fn(string $k): array => ['key' => $k, 'label' => enginesTierLabel($k)],
|
|
ToolModels::tierEngineKeys()
|
|
);
|
|
$reviewerModels = array_map(
|
|
static fn(string $k): array => ['key' => $k, 'label' => enginesReviewerLabel($k)],
|
|
dbnToolsReviewerModelKeys()
|
|
);
|
|
|
|
$rows = [];
|
|
$row = static function (string $tool, string $scope) use ($overrides): array {
|
|
$key = $tool . '|' . $scope;
|
|
$ov = $overrides[$key]['engine'] ?? null;
|
|
$def = enginesCodeDefault($scope);
|
|
return [
|
|
'tool_slug' => $tool,
|
|
'scope' => $scope,
|
|
'code_default' => $def,
|
|
'override' => $ov,
|
|
'effective' => $ov ?? $def,
|
|
'updated_by' => $overrides[$key]['updated_by'] ?? null,
|
|
'updated_at' => $overrides[$key]['updated_at'] ?? null,
|
|
];
|
|
};
|
|
|
|
// Global tier defaults (apply to every tier tool unless a per-tool row overrides).
|
|
$rows[] = $row('*', 'tier_quick');
|
|
$rows[] = $row('*', 'tier_pro');
|
|
// Per-tool tier overrides.
|
|
foreach (ENGINES_TIER_TOOLS as $tool) {
|
|
$rows[] = $row($tool, 'tier_quick');
|
|
$rows[] = $row($tool, 'tier_pro');
|
|
}
|
|
// Persona reviewer (legal-analysis track).
|
|
$rows[] = $row('*', 'persona_family');
|
|
$rows[] = $row('*', 'persona_general');
|
|
|
|
dbnToolsRespond([
|
|
'ok' => true,
|
|
'context' => [
|
|
'bedrock_enabled' => filter_var(dbnToolsEnv('DBN_BEDROCK_ENABLED', 'false'), FILTER_VALIDATE_BOOLEAN),
|
|
'note' => 'claude_* engines route to AWS Bedrock when DBN_BEDROCK_ENABLED=true, else fall back to Azure GPT. GPU fine-tunes degrade to gpt-4o when the pod is offline.',
|
|
],
|
|
'tier_engines' => $tierEngines,
|
|
'reviewer_models' => $reviewerModels,
|
|
'tier_tools' => ENGINES_TIER_TOOLS,
|
|
'rows' => $rows,
|
|
]);
|
|
}
|
|
|
|
function enginesSet(): void
|
|
{
|
|
$input = dbnToolsJsonInput(2_000);
|
|
$tool = trim((string)($input['tool_slug'] ?? ''));
|
|
$scope = trim((string)($input['scope'] ?? ''));
|
|
$engine = trim((string)($input['engine'] ?? ''));
|
|
|
|
if ($tool === '' || !preg_match('/^[a-z0-9*\-_.]+$/', $tool)) {
|
|
dbnToolsError('Invalid tool slug.', 400, 'bad_tool');
|
|
}
|
|
if (!enginesValidScope($scope)) {
|
|
dbnToolsError('Invalid scope.', 400, 'bad_scope');
|
|
}
|
|
if (!enginesValidEngineForScope($scope, $engine)) {
|
|
dbnToolsError('Engine is not valid for this scope.', 400, 'bad_engine');
|
|
}
|
|
|
|
$by = strtolower((string)($_SESSION['dbn_tools_user_email'] ?? $_SESSION['dbn_tools_sso_email'] ?? 'owner'));
|
|
$stmt = dbnmDb()->prepare(
|
|
'INSERT INTO dbn_tool_engine_config (tool_slug, scope, engine, enabled, updated_by)
|
|
VALUES (?, ?, ?, 1, ?)
|
|
ON DUPLICATE KEY UPDATE engine = VALUES(engine), enabled = 1, updated_by = VALUES(updated_by)'
|
|
);
|
|
$stmt->execute([$tool, $scope, $engine, $by]);
|
|
|
|
dbnToolsRespond(['ok' => true, 'tool_slug' => $tool, 'scope' => $scope, 'engine' => $engine]);
|
|
}
|
|
|
|
function enginesClear(): void
|
|
{
|
|
$input = dbnToolsJsonInput(2_000);
|
|
$tool = trim((string)($input['tool_slug'] ?? ''));
|
|
$scope = trim((string)($input['scope'] ?? ''));
|
|
|
|
if ($tool === '' || !enginesValidScope($scope)) {
|
|
dbnToolsError('Invalid tool or scope.', 400, 'bad_request');
|
|
}
|
|
|
|
$stmt = dbnmDb()->prepare('DELETE FROM dbn_tool_engine_config WHERE tool_slug = ? AND scope = ?');
|
|
$stmt->execute([$tool, $scope]);
|
|
|
|
dbnToolsRespond(['ok' => true, 'cleared' => true, 'tool_slug' => $tool, 'scope' => $scope]);
|
|
}
|