Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
namespace OCA\OpenAi;

use OCA\OpenAi\AppInfo\Application;
use OCA\OpenAi\Service\OpenAiAPIService;
use OCA\OpenAi\Service\OpenAiSettingsService;
use OCP\Capabilities\IPublicCapability;

class Capabilities implements IPublicCapability {
public function __construct(
private OpenAiAPIService $openAiAPIService,
private OpenAiSettingsService $openAiSettingsService,
) {
}

public function getCapabilities(): array {
return [
Application::APP_ID => [
'uses_openai' => $this->openAiAPIService->isUsingOpenAi(),
'uses_openai' => $this->openAiSettingsService->isUsingOpenAi(),
],
];
}
Expand Down
4 changes: 2 additions & 2 deletions lib/OldProcessing/Translation/TranslationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function detectLanguage(string $text): ?string {
$prompt = 'What language is this (answer with the language name only, in English): ' . $text;
$adminModel = $this->openAiSettingsService->getAdminDefaultCompletionModelId();
try {
if ($this->openAiAPIService->isUsingOpenAi() || $this->openAiSettingsService->getChatEndpointEnabled()) {
if ($this->openAiSettingsService->isUsingOpenAi() || $this->openAiSettingsService->getChatEndpointEnabled()) {
$completion = $this->openAiAPIService->createChatCompletion($this->userId, $adminModel, $prompt, null, null, 1, 100);
$completion = $completion['messages'];
} else {
Expand Down Expand Up @@ -107,7 +107,7 @@ public function translate(?string $fromLanguage, string $toLanguage, string $tex
}
$adminModel = $this->openAiSettingsService->getAdminDefaultCompletionModelId();

if ($this->openAiAPIService->isUsingOpenAi() || $this->openAiSettingsService->getChatEndpointEnabled()) {
if ($this->openAiSettingsService->isUsingOpenAi() || $this->openAiSettingsService->getChatEndpointEnabled()) {
$completion = $this->openAiAPIService->createChatCompletion($this->userId, $adminModel, $prompt, null, null, 1, PHP_INT_MAX);
$completion = $completion['messages'];
} else {
Expand Down
73 changes: 17 additions & 56 deletions lib/Service/OpenAiAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,52 +74,13 @@ public function createQuotaUsage(string $userId, int $type, int $usage) {
$this->quotaUsageMapper->createQuotaUsage($userId, $type, $usage, $rule['pool'] ? $rule['id'] : -1);
}

/**
* @param ?string $serviceType
* @return bool
*/
public function isUsingOpenAi(?string $serviceType = null): bool {
$serviceUrl = '';
if ($serviceType === Application::SERVICE_TYPE_IMAGE) {
$serviceUrl = $this->openAiSettingsService->getImageServiceUrl();
} elseif ($serviceType === Application::SERVICE_TYPE_STT) {
$serviceUrl = $this->openAiSettingsService->getSttServiceUrl();
} elseif ($serviceType === Application::SERVICE_TYPE_TTS) {
$serviceUrl = $this->openAiSettingsService->getTtsServiceUrl();
}
if ($serviceUrl === '') {
$serviceUrl = $this->openAiSettingsService->getServiceUrl();
}
return $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL;
}

/**
* @param ?string $serviceType
* @return bool
*/
public function isUsingOpenRouter(?string $serviceType = null): bool {
$serviceUrl = '';
if ($serviceType === Application::SERVICE_TYPE_IMAGE) {
$serviceUrl = $this->openAiSettingsService->getImageServiceUrl();
} elseif ($serviceType === Application::SERVICE_TYPE_STT) {
$serviceUrl = $this->openAiSettingsService->getSttServiceUrl();
} elseif ($serviceType === Application::SERVICE_TYPE_TTS) {
$serviceUrl = $this->openAiSettingsService->getTtsServiceUrl();
}
if ($serviceUrl === '') {
$serviceUrl = $this->openAiSettingsService->getServiceUrl();
}
// Return true if the service URL references OpenRouter (e.g., openrouter.ai)
return str_starts_with(strtolower($serviceUrl), 'https://openrouter.ai');
}

/**
* @param ?string $serviceType
*
* @return string
*/
public function getServiceName(?string $serviceType = null): string {
if ($this->isUsingOpenAi($serviceType)) {
if ($this->openAiSettingsService->isUsingOpenAi($serviceType)) {
if ($serviceType === Application::SERVICE_TYPE_IMAGE) {
return $this->l10n->t('OpenAI\'s Image Generation');
}
Expand Down Expand Up @@ -243,7 +204,7 @@ public function getModels(?string $userId, bool $refresh = false, ?string $servi

try {
$this->logger->debug('Actually getting OpenAI models with a network request');
$params = $this->isUsingOpenRouter($serviceType) ? ['output_modalities' => 'all'] : [];
$params = $this->openAiSettingsService->isUsingOpenRouter($serviceType) ? ['output_modalities' => 'all'] : [];
$modelsResponse = $this->request($userId, 'models', $params, serviceType: $serviceType);
} catch (Exception $e) {
$this->logger->warning('Error retrieving models (exc): ' . $e->getMessage());
Expand Down Expand Up @@ -275,7 +236,7 @@ public function getModels(?string $userId, bool $refresh = false, ?string $servi
* @param string $userId
*/
private function hasOwnOpenAiApiKey(string $userId): bool {
if (!$this->isUsingOpenAi()) {
if (!$this->openAiSettingsService->isUsingOpenAi()) {
return false;
}

Expand All @@ -296,7 +257,7 @@ public function getModelEnumValues(?string $userId, ?string $serviceType = null)
$modelEnumValues = array_map(function (array $model) {
return new ShapeEnumValue($model['id'], $model['id']);
}, $modelResponse['data'] ?? []);
if ($this->isUsingOpenAi()) {
if ($this->openAiSettingsService->isUsingOpenAi()) {
array_unshift($modelEnumValues, new ShapeEnumValue($this->l10n->t('Default'), 'Default'));
}
return $modelEnumValues;
Expand Down Expand Up @@ -722,7 +683,7 @@ private function buildChatCompletionRequestParams(
$messages[] = [
// o1-* models don't support system messages
// system prompts as a user message seems to work fine though
'role' => ($this->isUsingOpenAi() && str_starts_with($modelRequestParam, 'o1-'))
'role' => ($this->openAiSettingsService->isUsingOpenAi() && str_starts_with($modelRequestParam, 'o1-'))
? 'user'
: 'system',
'content' => $systemPrompt,
Expand Down Expand Up @@ -845,7 +806,7 @@ private function buildChatCompletionRequestParams(
if ($tools !== null) {
$params['tools'] = $tools;
}
if ($userId !== null && $this->isUsingOpenAi()) {
if ($userId !== null && $this->openAiSettingsService->isUsingOpenAi()) {
$params['user'] = $userId;
}

Expand All @@ -856,7 +817,7 @@ private function buildChatCompletionRequestParams(
if ($extraParams !== null) {
$params = array_merge($extraParams, $params);
}
if ($stream && $this->isUsingOpenAi()) {
if ($stream && $this->openAiSettingsService->isUsingOpenAi()) {
$params['stream_options'] = array_merge(
is_array($params['stream_options'] ?? null) ? $params['stream_options'] : [],
['include_usage' => true],
Expand Down Expand Up @@ -953,7 +914,7 @@ public function transcribe(
throw new Exception($this->l10n->t('Audio transcription quota exceeded'), Http::STATUS_TOO_MANY_REQUESTS);
}
// enforce whisper for OpenAI
if ($this->isUsingOpenAi()) {
if ($this->openAiSettingsService->isUsingOpenAi()) {
$model = Application::DEFAULT_TRANSCRIPTION_MODEL_ID;
}

Expand Down Expand Up @@ -1158,7 +1119,7 @@ public function requestSpeechCreation(
* @return int
*/
public function getExpTextProcessingTime(): int {
return $this->isUsingOpenAi()
return $this->openAiSettingsService->isUsingOpenAi()
? intval($this->appConfig->getValueString(Application::APP_ID, 'openai_text_generation_time', strval(Application::DEFAULT_OPENAI_TEXT_GENERATION_TIME), lazy: true))
: intval($this->appConfig->getValueString(Application::APP_ID, 'localai_text_generation_time', strval(Application::DEFAULT_LOCALAI_TEXT_GENERATION_TIME), lazy: true));
}
Expand All @@ -1171,7 +1132,7 @@ public function updateExpTextProcessingTime(int $runtime): void {
$oldTime = floatval($this->getExpTextProcessingTime());
$newTime = (1.0 - Application::EXPECTED_RUNTIME_LOWPASS_FACTOR) * $oldTime + Application::EXPECTED_RUNTIME_LOWPASS_FACTOR * floatval($runtime);

if ($this->isUsingOpenAi()) {
if ($this->openAiSettingsService->isUsingOpenAi()) {
$this->appConfig->setValueString(Application::APP_ID, 'openai_text_generation_time', strval(intval($newTime)), lazy: true);
} else {
$this->appConfig->setValueString(Application::APP_ID, 'localai_text_generation_time', strval(intval($newTime)), lazy: true);
Expand All @@ -1182,7 +1143,7 @@ public function updateExpTextProcessingTime(int $runtime): void {
* @return int
*/
public function getExpImgProcessingTime(): int {
return $this->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE)
return $this->openAiSettingsService->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE)
? intval($this->appConfig->getValueString(Application::APP_ID, 'openai_image_generation_time', strval(Application::DEFAULT_OPENAI_IMAGE_GENERATION_TIME), lazy: true))
: intval($this->appConfig->getValueString(Application::APP_ID, 'localai_image_generation_time', strval(Application::DEFAULT_LOCALAI_IMAGE_GENERATION_TIME), lazy: true));
}
Expand All @@ -1195,7 +1156,7 @@ public function updateExpImgProcessingTime(int $runtime): void {
$oldTime = floatval($this->getExpImgProcessingTime());
$newTime = (1.0 - Application::EXPECTED_RUNTIME_LOWPASS_FACTOR) * $oldTime + Application::EXPECTED_RUNTIME_LOWPASS_FACTOR * floatval($runtime);

if ($this->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE)) {
if ($this->openAiSettingsService->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE)) {
$this->appConfig->setValueString(Application::APP_ID, 'openai_image_generation_time', strval(intval($newTime)), lazy: true);
} else {
$this->appConfig->setValueString(Application::APP_ID, 'localai_image_generation_time', strval(intval($newTime)), lazy: true);
Expand Down Expand Up @@ -1242,7 +1203,7 @@ public function request(
return ['error' => 'An API key is required for api.openai.com'];
}

if ($this->isUsingOpenAi($serviceType) || !$useBasicAuth) {
if ($this->openAiSettingsService->isUsingOpenAi($serviceType) || !$useBasicAuth) {
if ($apiKey !== '') {
$options['headers']['Authorization'] = 'Bearer ' . $apiKey;
}
Expand All @@ -1252,7 +1213,7 @@ public function request(
}
}

if (!$this->isUsingOpenAi($serviceType)) {
if (!$this->openAiSettingsService->isUsingOpenAi($serviceType)) {
$options['nextcloud']['allow_local_address'] = true;
}

Expand Down Expand Up @@ -1555,7 +1516,7 @@ private function normalizeChatCompletionResponse(array $response): array {
* @return bool whether the T2I provider is available
*/
public function isT2IAvailable(): bool {
if ($this->openAiSettingsService->imageOverrideEnabled() || $this->isUsingOpenAi()) {
if ($this->openAiSettingsService->imageOverrideEnabled() || $this->openAiSettingsService->isUsingOpenAi()) {
return true;
}
try {
Expand All @@ -1576,7 +1537,7 @@ public function isT2IAvailable(): bool {
* @return bool whether the STT provider is available
*/
public function isSTTAvailable(): bool {
if ($this->openAiSettingsService->sttOverrideEnabled() || $this->isUsingOpenAi()) {
if ($this->openAiSettingsService->sttOverrideEnabled() || $this->openAiSettingsService->isUsingOpenAi()) {
return true;
}
try {
Expand All @@ -1597,7 +1558,7 @@ public function isSTTAvailable(): bool {
* @return bool whether the TTS provider is available
*/
public function isTTSAvailable(): bool {
if ($this->openAiSettingsService->ttsOverrideEnabled() || $this->isUsingOpenAi()) {
if ($this->openAiSettingsService->ttsOverrideEnabled() || $this->openAiSettingsService->isUsingOpenAi()) {
return true;
}
try {
Expand Down
25 changes: 15 additions & 10 deletions lib/Service/OpenAiFileService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

namespace OCA\OpenAi\Service;

use OCA\OpenAi\AppInfo\Application;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IL10N;
Expand Down Expand Up @@ -119,7 +118,7 @@ public function buildFileContentFromFile(?File $file): array {
throw new ProcessingException('File is not readable');
}
// Maximum file size for openai is 50MB.
if ($this->isUsingOpenAi() && $file->getSize() > self::MAX_FILE_SIZE_BYTES) {
if ($this->openAiSettingsService->isUsingOpenAi() && $file->getSize() > self::MAX_FILE_SIZE_BYTES) {
throw new UserFacingProcessingException(
'Filesize of input files too large. Max is 50MB',
0,
Expand Down Expand Up @@ -160,7 +159,7 @@ private function buildImageContent(File $file, string $fileType): array {
$this->l10n->t('Image attachments are unsupported.'),
);
}
if ($this->isUsingOpenAi() && !in_array($fileType, self::VALID_IMAGE_MIME_TYPES, true)) {
if ($this->openAiSettingsService->isUsingOpenAi() && !in_array($fileType, self::VALID_IMAGE_MIME_TYPES, true)) {
throw new UserFacingProcessingException(
'Invalid input file type for OpenAI ' . $fileType,
0,
Expand Down Expand Up @@ -228,7 +227,7 @@ private function buildVideoContent(File $file, string $fileType): array {
}

/**
* @return list<array{type: string, file: array{filename: string, file_data: string}}>
* @return list<array<string, mixed>>
*/
private function buildDocumentContent(File $file, string $fileType): array {
if (!$this->openAiSettingsService->getMultimodalDocumentEnabled()) {
Expand All @@ -239,11 +238,22 @@ private function buildDocumentContent(File $file, string $fileType): array {
$this->l10n->t('Document attachments are unsupported.'),
);
}
$dataUri = 'data:' . $fileType . ';base64,' . base64_encode(stream_get_contents($file->fopen('rb')));

if ($this->openAiSettingsService->isUsingMistral()) {
// Mistral does not accept the default openai shape so we use a fallback for them
return [[
'type' => 'document_url',
'document_url' => $dataUri,
'document_name' => $file->getName(),
]];
}

return [[
'type' => 'file',
'file' => [
'filename' => $file->getName(),
'file_data' => 'data:' . $fileType . ';base64,' . base64_encode(stream_get_contents($file->fopen('rb'))),
'file_data' => $dataUri,
],
]];
}
Expand All @@ -266,9 +276,4 @@ private function buildTextContent(File $file, string $fileType): array {
'text' => 'Filename:' . $file->getName() . "\nContent:\n" . stream_get_contents($file->fopen('rb')),
]];
}

private function isUsingOpenAi(): bool {
$serviceUrl = $this->openAiSettingsService->getServiceUrl();
return $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL;
}
}
59 changes: 48 additions & 11 deletions lib/Service/OpenAiSettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,51 @@ public function getServiceUrl(): string {
return $this->appConfig->getValueString(Application::APP_ID, 'url');
}

/**
* Resolve the effective service URL for a service type, falling back to the main URL.
*
* @param ?string $serviceType
*/
private function resolveServiceUrl(?string $serviceType = null): string {
$serviceUrl = '';
if ($serviceType === Application::SERVICE_TYPE_IMAGE) {
$serviceUrl = $this->getImageServiceUrl();
} elseif ($serviceType === Application::SERVICE_TYPE_STT) {
$serviceUrl = $this->getSttServiceUrl();
} elseif ($serviceType === Application::SERVICE_TYPE_TTS) {
$serviceUrl = $this->getTtsServiceUrl();
}
if ($serviceUrl === '') {
$serviceUrl = $this->getServiceUrl();
}
return $serviceUrl;
}

/**
* @param ?string $serviceType
*/
public function isUsingOpenAi(?string $serviceType = null): bool {
$serviceUrl = $this->resolveServiceUrl($serviceType);
return $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL;
}

/**
* @param ?string $serviceType
*/
public function isUsingOpenRouter(?string $serviceType = null): bool {
$serviceUrl = $this->resolveServiceUrl($serviceType);
// Return true if the service URL references OpenRouter (e.g., openrouter.ai)
return str_starts_with(strtolower($serviceUrl), 'https://openrouter.ai');
}

/**
* @param ?string $serviceType
*/
public function isUsingMistral(?string $serviceType = null): bool {
$serviceUrl = $this->resolveServiceUrl($serviceType);
return str_starts_with(strtolower($serviceUrl), 'https://api.mistral.ai');
}

/**
* @return string
*/
Expand Down Expand Up @@ -630,7 +675,7 @@ public function getAdminConfig(): array {
* @return array{api_key: string, basic_password: string, basic_user: string, is_custom_service: bool, use_basic_auth: bool, stt_language: string}
*/
public function getUserConfig(string $userId): array {
$isCustomService = $this->getServiceUrl() !== '' && $this->getServiceUrl() !== Application::OPENAI_API_BASE_URL;
$isCustomService = !$this->isUsingOpenAi();
return [
'api_key' => $this->getUserApiKey($userId),
'basic_user' => $this->getUserBasicUser($userId, false),
Expand All @@ -645,10 +690,8 @@ public function getUserConfig(string $userId): array {
* @return bool
*/
public function getUseMaxCompletionTokensParam(): bool {
$serviceUrl = $this->getServiceUrl();
$isUsingOpenAI = $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL;
// we know OpenAI expects "use_max_completion_tokens_param", let's assume the other services don't
$default = $isUsingOpenAI ? '1' : '0';
$default = $this->isUsingOpenAi() ? '1' : '0';
return $this->appConfig->getValueString(Application::APP_ID, 'use_max_completion_tokens_param', $default, lazy: true) === '1';
}

Expand All @@ -663,14 +706,8 @@ public function getTranslationProviderEnabled(): bool {
* @return bool
*/
public function getIsImageRetrievalAuthenticated(): bool {
$serviceUrl = $this->getServiceUrl();
// the image_request_auth default depends on the service used for image generation
// if we override it, we check the one we are really gonna use
if ($this->imageOverrideEnabled()) {
$serviceUrl = $this->getImageServiceUrl();
}
$isUsingOpenAI = $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL;
$default = $isUsingOpenAI ? '0' : '1';
$default = $this->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE) ? '0' : '1';
return $this->appConfig->getValueString(Application::APP_ID, 'image_request_auth', $default, lazy: true) === '1';
}

Expand Down
Loading
Loading