256 lines
9.9 KiB
PHP
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();
|
|
}
|
|
} |