Files
SkinbaseNova/app/Console/Commands/AiBiographyProvidersCommand.php
2026-04-18 17:02:56 +02:00

256 lines
9.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
final class AiBiographyProvidersCommand extends Command
{
protected $signature = 'ai-biography:providers
{--provider= : Inspect only one provider (together|vision_gateway|vision|gemini|home)}
{--limit=10 : Maximum number of models to display per provider}';
protected $description = 'Check AI biography provider health and list available models';
public function handle(): int
{
$provider = $this->resolveProviderOption();
if ($provider === false) {
return self::FAILURE;
}
$limit = max(1, (int) $this->option('limit'));
$providers = $provider !== null
? [$provider]
: ['together', 'vision_gateway', 'gemini', 'home'];
$inspections = array_map(fn (string $name): array => $this->inspectProvider($name, $limit), $providers);
$this->table(
['Provider', 'Status', 'Configured model', 'Models endpoint'],
array_map(fn (array $inspection): array => [
$inspection['provider'],
$inspection['status'],
$inspection['configured_model'] ?: 'n/a',
$inspection['models_endpoint'] ?: 'n/a',
], $inspections)
);
foreach ($inspections as $inspection) {
$this->line('');
$this->comment(strtoupper($inspection['provider']));
$this->line(' Base URL : ' . ($inspection['base_url'] ?: 'n/a'));
$this->line(' Configured : ' . ($inspection['configured'] ? 'yes' : 'no'));
$this->line(' Status : ' . $inspection['status']);
if ($inspection['error'] !== null) {
$this->line(' Error : ' . $inspection['error']);
}
if ($inspection['models'] === []) {
$this->line(' Models : none reported');
continue;
}
$this->line(' Models');
foreach ($inspection['models'] as $model) {
$this->line(' - ' . $model);
}
}
return self::SUCCESS;
}
private function inspectProvider(string $provider, int $limit): array
{
$baseUrl = $this->providerBaseUrl($provider);
$configuredModel = $this->configuredModel($provider);
$modelsEndpoint = $this->modelsEndpoint($provider, $baseUrl);
if (! $this->isConfigured($provider, $baseUrl, $configuredModel)) {
return [
'provider' => $provider,
'status' => 'not_configured',
'configured' => false,
'configured_model' => $configuredModel,
'base_url' => $baseUrl,
'models_endpoint' => $modelsEndpoint,
'models' => [],
'error' => 'Required configuration is missing.',
];
}
try {
$response = $this->buildRequest($provider)->get($modelsEndpoint);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
return [
'provider' => $provider,
'status' => 'offline',
'configured' => true,
'configured_model' => $configuredModel,
'base_url' => $baseUrl,
'models_endpoint' => $modelsEndpoint,
'models' => [],
'error' => $e->getMessage(),
];
}
if (! $response->successful()) {
return [
'provider' => $provider,
'status' => 'http_' . $response->status(),
'configured' => true,
'configured_model' => $configuredModel,
'base_url' => $baseUrl,
'models_endpoint' => $modelsEndpoint,
'models' => [],
'error' => mb_substr(trim($response->body()), 0, 300),
];
}
return [
'provider' => $provider,
'status' => 'online',
'configured' => true,
'configured_model' => $configuredModel,
'base_url' => $baseUrl,
'models_endpoint' => $modelsEndpoint,
'models' => array_slice($this->extractModels($provider, $response), 0, $limit),
'error' => null,
];
}
private function resolveProviderOption(): string|false|null
{
$rawProvider = $this->option('provider');
if ($rawProvider === null || trim((string) $rawProvider) === '') {
return null;
}
return match (strtolower(trim((string) $rawProvider))) {
'vision_gateway', 'vision', 'local' => 'vision_gateway',
'gemini' => 'gemini',
'home', 'lmstudio', 'lm_studio' => 'home',
'together', 'together_ai' => 'together',
default => $this->invalidProvider((string) $rawProvider),
};
}
private function invalidProvider(string $provider): false
{
$this->error("Invalid provider [{$provider}]. Supported values: together, vision_gateway, vision, gemini, home.");
return false;
}
private function isConfigured(string $provider, string $baseUrl, string $configuredModel): bool
{
return match ($provider) {
'vision_gateway' => $baseUrl !== '' && trim((string) config('vision.gateway.api_key', '')) !== '',
'gemini' => $baseUrl !== '' && trim((string) config('ai_biography.gemini.api_key', '')) !== '' && $configuredModel !== '',
'home' => $baseUrl !== '' && $configuredModel !== '',
'together' => trim((string) config('ai_biography.together.api_key', '')) !== '' && $configuredModel !== '',
default => false,
};
}
private function configuredModel(string $provider): string
{
return match ($provider) {
'vision_gateway' => trim((string) config('ai_biography.llm_model', 'vision-gateway')),
'gemini' => trim((string) config('ai_biography.gemini.model', 'gemini-flash-latest')),
'home' => trim((string) config('ai_biography.home.model', 'qwen/qwen3.5-9b')),
'together' => trim((string) config('ai_biography.together.model', 'google/gemma-3n-E4B-it')),
default => '',
};
}
private function providerBaseUrl(string $provider): string
{
return match ($provider) {
'vision_gateway' => rtrim((string) config('vision.gateway.base_url', ''), '/'),
'gemini' => rtrim((string) config('ai_biography.gemini.base_url', 'https://generativelanguage.googleapis.com'), '/'),
'home' => rtrim((string) config('ai_biography.home.base_url', 'http://home.klevze.si:8200'), '/'),
'together' => rtrim((string) config('ai_biography.together.base_url', 'https://api.together.xyz'), '/'),
default => '',
};
}
private function modelsEndpoint(string $provider, string $baseUrl): string
{
if ($baseUrl === '') {
return '';
}
return match ($provider) {
'gemini' => $baseUrl . '/v1beta/models',
default => $baseUrl . '/v1/models',
};
}
private function buildRequest(string $provider): PendingRequest
{
$request = Http::acceptJson()->contentType('application/json');
return match ($provider) {
'vision_gateway' => $request
->withHeaders(['X-API-Key' => (string) config('vision.gateway.api_key', '')])
->connectTimeout(max(1, (int) config('vision.gateway.connect_timeout_seconds', 3)))
->timeout(max(5, (int) config('ai_biography.llm_timeout_seconds', 30))),
'gemini' => $request
->withHeaders(['X-goog-api-key' => (string) config('ai_biography.gemini.api_key', '')])
->connectTimeout(max(1, (int) config('vision.gateway.connect_timeout_seconds', 3)))
->timeout(max(5, (int) config('ai_biography.llm_timeout_seconds', 30))),
'together' => $request
->withToken((string) config('ai_biography.together.api_key', ''))
->connectTimeout(max(1, (int) config('ai_biography.together.connect_timeout_seconds', 5)))
->timeout(max(5, (int) config('ai_biography.together.timeout_seconds', config('ai_biography.llm_timeout_seconds', 90)))),
'home' => $this->buildHomeRequest($request),
default => $request,
};
}
private function buildHomeRequest(PendingRequest $request): PendingRequest
{
$request = $request
->connectTimeout(max(1, (int) config('ai_biography.home.connect_timeout_seconds', 3)))
->timeout(max(5, (int) config('ai_biography.home.timeout_seconds', config('ai_biography.llm_timeout_seconds', 30))));
if (! (bool) config('ai_biography.home.verify_ssl', true)) {
$request = $request->withoutVerifying();
}
$apiKey = trim((string) config('ai_biography.home.api_key', ''));
return $apiKey !== '' ? $request->withToken($apiKey) : $request;
}
/**
* @return list<string>
*/
private function extractModels(string $provider, Response $response): array
{
$json = $response->json();
if ($provider === 'gemini') {
return collect((array) ($json['models'] ?? []))
->map(fn ($model) => is_array($model) ? (string) ($model['name'] ?? $model['displayName'] ?? '') : '')
->filter(fn (string $name): bool => $name !== '')
->values()
->all();
}
return collect((array) ($json['data'] ?? []))
->map(fn ($model) => is_array($model) ? (string) ($model['id'] ?? '') : '')
->filter(fn (string $name): bool => $name !== '')
->values()
->all();
}
}