seedproject-web/api/app/LLM/LLMManager.php
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

254 lines
8.2 KiB
PHP

<?php
namespace App\LLM;
/**
* Loads LLM provider config and returns a configured, ready-to-use provider instance.
*
* Two scopes:
* Application-level — stored in sp_settings (group='llm'), admin-managed, shared across all orgs.
* Org-level — stored in sp_orgs_meta, per-org overrides (optional future use).
*
* Usage:
* $llm = LLMManager::forApp('anthropic'); // application-level (sp_settings)
* $llm = LLMManager::forOrg($orgId, 'openai'); // org-level (sp_orgs_meta)
*/
class LLMManager
{
// Maps provider key to provider class
private static array $providers = [
'openai' => 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,
]);
}
}
}