diff --git a/README.md b/README.md index 94ce2386..5dd9ae1a 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,13 @@ They might work when connecting to other services, without any guarantee. Instead of connecting to the OpenAI API for these, you can also connect to a self-hosted [LocalAI](https://localai.io) instance or [Ollama](https://ollama.com/) instance or to any service that implements an API similar to the OpenAI one, for example: -[IONOS AI Model Hub](https://docs.ionos.com/cloud/ai/ai-model-hub), [Plusserver](https://www.plusserver.com/en/ai-platform/) or [MistralAI](https://mistral.ai). +[IONOS AI Model Hub](https://docs.ionos.com/cloud/ai/ai-model-hub), [Plusserver](https://www.plusserver.com/en/ai-platform/), [MistralAI](https://mistral.ai) or [Eden AI](https://www.edenai.co). Make sure to use the OpenAI-compatible endpoint instead of the custom ones they provide. +Eden AI additionally offers a European endpoint, `https://api.eu.edenai.run/v3`, which only exposes +models cleared for processing in the EU and refuses anything served elsewhere. Use it as the service +URL when your instance needs the data to stay in Europe. + :warning: This app is mainly tested with OpenAI. We do not guarantee it works perfectly with other services that implement OpenAI-compatible APIs with slight differences. diff --git a/lib/Service/OpenAiAPIService.php b/lib/Service/OpenAiAPIService.php index a52e6242..56d121b0 100644 --- a/lib/Service/OpenAiAPIService.php +++ b/lib/Service/OpenAiAPIService.php @@ -128,9 +128,52 @@ public function getModels(?string $userId, ServiceConfig $service): array { throw new Exception($this->l10n->t('Invalid models response received'), Http::STATUS_INTERNAL_SERVER_ERROR); } + if ($service->isUsingEdenAi()) { + $modelsResponse['data'] = array_merge( + $modelsResponse['data'], + $this->getEdenAiAudioModels($userId, $service), + ); + } + return $modelsResponse; } + /** + * Eden AI keeps one catalogue per modality, so its main models endpoint + * returns the text models only. Speech to text and text to speech have + * their own endpoints, and without them the transcription and speech model + * selectors in the admin settings have nothing to offer. + * + * A failure here is not fatal. The text models have already been retrieved + * and are usable on their own, so we log and return what we have. + * + * @param ?string $userId + * @param ServiceConfig $service + * @return list the audio models, or an empty list + */ + private function getEdenAiAudioModels(?string $userId, ServiceConfig $service): array { + $models = []; + foreach (['audio/transcriptions/models', 'audio/speech/models'] as $endPoint) { + try { + $response = $this->request($userId, $service, $endPoint); + } catch (Exception $e) { + $this->logger->warning('Error retrieving Eden AI models from ' . $endPoint . ' (exc): ' . $e->getMessage()); + continue; + } + if (isset($response['error']) || !isset($response['data']) || !is_array($response['data'])) { + $this->logger->warning('Error retrieving Eden AI models from ' . $endPoint . ': ' . json_encode($response)); + continue; + } + foreach ($response['data'] as $model) { + if (is_array($model) && isset($model['id'])) { + $models[] = $model; + } + } + } + + return $models; + } + /** * Check whether quota is exceeded for a user * diff --git a/lib/Service/ServiceConfig.php b/lib/Service/ServiceConfig.php index 0bd8b7bf..a978be25 100644 --- a/lib/Service/ServiceConfig.php +++ b/lib/Service/ServiceConfig.php @@ -281,6 +281,16 @@ public function isUsingIonos(): bool { return str_starts_with(strtolower($this->url), 'https://openai.inference.de-txl.ionos.com'); } + /** + * Eden AI serves the global endpoint and an EU one that only exposes + * models cleared for European processing. Both behave the same way here. + */ + public function isUsingEdenAi(): bool { + $url = strtolower($this->url); + return str_starts_with($url, 'https://api.edenai.run') + || str_starts_with($url, 'https://api.eu.edenai.run'); + } + public function getApiKey(): string { return $this->apiKey; } diff --git a/tests/unit/Service/MultiServiceTest.php b/tests/unit/Service/MultiServiceTest.php index f1cb0228..704a03e3 100644 --- a/tests/unit/Service/MultiServiceTest.php +++ b/tests/unit/Service/MultiServiceTest.php @@ -509,4 +509,20 @@ public function testProviderIdsEndInTheirTaskTypeId(): void { } $this->assertGreaterThan(15, $checked, 'the factory should have built a provider per task type'); } + + public function testEdenAiServiceIsRecognisedOnBothEndpoints(): void { + $global = $this->addService(['url' => 'https://api.edenai.run/v3']); + $this->assertTrue($global->isUsingEdenAi()); + $this->assertFalse($global->isUsingOpenAi()); + + // the European endpoint only exposes models cleared for EU processing + $european = $this->addService(['url' => 'https://api.eu.edenai.run/v3']); + $this->assertTrue($european->isUsingEdenAi()); + + $mixedCase = $this->addService(['url' => 'HTTPS://API.EDENAI.RUN/v3']); + $this->assertTrue($mixedCase->isUsingEdenAi()); + + $other = $this->addService(['url' => self::IMAGE_BASE]); + $this->assertFalse($other->isUsingEdenAi()); + } }