'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]); }