svcOpenAI::class, 'anthropic' => svcAnthropic::class, 'gemini' => svcGemini::class, 'mistral' => svcMistral::class, 'deepseek' => svcDeepSeek::class, ]; // Maps provider key to sp_settings.keyval (application-level) private static array $settingsKeys = [ 'openai' => 'llm_openai', 'anthropic' => 'llm_anthropic', 'gemini' => 'llm_gemini', 'mistral' => 'llm_mistral', 'deepseek' => 'llm_deepseek', ]; // Maps provider key to sp_orgs_meta.keyval (org-level overrides) private static array $metaKeys = [ 'openai' => 'llmOpenAI', 'anthropic' => 'llmAnthropic', 'gemini' => 'llmGemini', 'mistral' => 'llmMistral', 'deepseek' => 'llmDeepSeek', ]; // Provider preference order for auto-selection public static array $priority = ['anthropic', 'openai', 'gemini', 'mistral', 'deepseek']; /** * Instantiate a provider directly from a pre-loaded config array. * Use this when you already have the config and don't need another DB lookup. * * @throws \Exception if provider unknown */ public static function make(string $provider, array $config): LLMProvider { $provider = strtolower($provider); if (!isset(self::$providers[$provider])) { throw new \Exception("Unknown LLM provider: '$provider'"); } $class = self::$providers[$provider]; return new $class($config); } // ─── Application-level (sp_settings) ──────────────────────────────────── /** * Return a configured provider instance from the application-level sp_settings table. * * @throws \Exception if provider unknown or not configured */ public static function forApp(string $provider): LLMProvider { $provider = strtolower($provider); if (!isset(self::$providers[$provider])) { throw new \Exception("Unknown LLM provider: '$provider'"); } $config = self::getAppConfig($provider); if (empty($config)) { throw new \Exception("No '$provider' configuration found. Add one under Admin → Integrations."); } $class = self::$providers[$provider]; return new $class($config); } /** * Read and normalize a provider's config from sp_settings. * Returns [] if not found or api_key is empty. */ public static function getAppConfig(string $provider): array { $provider = strtolower($provider); $settingsKey = self::$settingsKeys[$provider] ?? null; if (!$settingsKey) return []; $row = \Db::getRow( "SELECT metval FROM sp_settings WHERE `group` = 'llm' AND keyval = ? LIMIT 1", [$settingsKey] ); if (!$row || empty($row['metval'])) return []; $raw = json_decode($row['metval'], true); if (!is_array($raw) || empty($raw['api_key'])) return []; // Normalize sp_settings field names to LLMProvider expected names return array_filter([ 'secretKey' => $raw['api_key'], 'model' => $raw['model'] ?? null, 'max_tokens' => $raw['max_tokens'] ?? null, 'temperature' => $raw['temperature'] ?? null, ]); } /** * Return the list of application-level providers that have a non-empty api_key. * * @return array e.g. ['anthropic', 'openai'] */ public static function getAppProviders(): array { $available = []; foreach (self::$settingsKeys as $provider => $keyval) { $row = \Db::getRow( "SELECT metval FROM sp_settings WHERE `group` = 'llm' AND keyval = ? LIMIT 1", [$keyval] ); if (!$row || empty($row['metval'])) continue; $cfg = json_decode($row['metval'], true); if (!empty($cfg['api_key'])) { $available[] = $provider; } } return $available; } /** * Return the first configured application-level provider (by priority), or null. */ public static function getDefaultAppProvider(): ?string { $available = self::getAppProviders(); foreach (self::$priority as $p) { if (in_array($p, $available)) return $p; } return null; } // ─── Org-level (sp_orgs_meta) ──────────────────────────────────────────── /** * Return a configured provider instance from sp_orgs_meta (org-level override). * * @throws \Exception if provider unknown or not configured for the org */ public static function forOrg(int $orgId, string $provider): LLMProvider { $provider = strtolower($provider); if (!isset(self::$providers[$provider])) { throw new \Exception("Unknown LLM provider: '$provider'"); } $config = self::getOrgConfig($orgId, $provider); if (empty($config)) { throw new \Exception("No '$provider' config found for org $orgId"); } $class = self::$providers[$provider]; return new $class($config); } /** * Read and decode a provider config from sp_orgs_meta for the given org. */ public static function getOrgConfig(int $orgId, string $provider): array { $provider = strtolower($provider); $metaKey = self::$metaKeys[$provider] ?? null; if (!$metaKey) return []; $row = \Db::getRow( "SELECT metval FROM sp_orgs_meta WHERE orgid = ? AND keyval = ? AND active = 1 LIMIT 1", [$orgId, $metaKey] ); if (!$row || empty($row['metval'])) return []; $config = json_decode($row['metval'], true); return is_array($config) ? $config : []; } /** * Return which providers are configured for the given org (sp_orgs_meta only). */ public static function getAvailableProviders(int $orgId): array { $metaKeys = array_values(self::$metaKeys); $rows = \Db::select( "SELECT keyval FROM sp_orgs_meta WHERE orgid = ? AND keyval IN ('" . implode("','", $metaKeys) . "') AND active = 1", [$orgId] ); $flip = array_flip(self::$metaKeys); $available = []; foreach ($rows as $row) { if (isset($flip[$row['keyval']])) { $available[] = $flip[$row['keyval']]; } } return $available; } /** * Save or update a provider config for an org in sp_orgs_meta. */ public static function saveOrgConfig(int $orgId, string $provider, array $config): void { $provider = strtolower($provider); $metaKey = self::$metaKeys[$provider] ?? null; if (!$metaKey) { throw new \Exception("Unknown LLM provider: '$provider'"); } $existing = \Db::getRow( "SELECT orgmetaid FROM sp_orgs_meta WHERE orgid = ? AND keyval = ? LIMIT 1", [$orgId, $metaKey] ); if ($existing) { \Db::update('sp_orgs_meta', ['metval' => json_encode($config)], 'orgmetaid = ?', [$existing['orgmetaid']] ); } else { \Db::insert('sp_orgs_meta', [ 'orgid' => $orgId, 'keyval' => $metaKey, 'metval' => json_encode($config), 'active' => 1, ]); } } }