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
254 lines
8.2 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|
|
}
|