From 70f733a4badca4de4215e9156aa5327381104ae8 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Sat, 6 Jun 2026 11:03:32 +0300 Subject: [PATCH 1/4] feat: add AI API support --- src/CrowdinApiClient/Api/AiApi.php | 648 +++++++++- src/CrowdinApiClient/Api/Enterprise/AiApi.php | 588 ++++++++- .../Model/AiCustomPlaceholder.php | 86 ++ .../Model/AiFineTuningDataset.php | 86 ++ .../Model/AiFineTuningEvent.php | 59 + .../Model/AiFineTuningJob.php | 86 ++ src/CrowdinApiClient/Model/AiPrompt.php | 197 +++ .../Model/AiPromptCompletion.php | 86 ++ src/CrowdinApiClient/Model/AiProvider.php | 134 ++ .../Model/AiProviderModel.php | 50 + .../Model/AiProxyChatCompletion.php | 13 + src/CrowdinApiClient/Model/AiReport.php | 86 ++ src/CrowdinApiClient/Model/AiSettings.php | 70 ++ src/CrowdinApiClient/Model/AiSnippet.php | 83 ++ tests/CrowdinApiClient/Api/AiApiTest.php | 1114 ++++++++++++++++- .../Api/Enterprise/AiApiTest.php | 1069 +++++++++++++++- 16 files changed, 4413 insertions(+), 42 deletions(-) create mode 100644 src/CrowdinApiClient/Model/AiCustomPlaceholder.php create mode 100644 src/CrowdinApiClient/Model/AiFineTuningDataset.php create mode 100644 src/CrowdinApiClient/Model/AiFineTuningEvent.php create mode 100644 src/CrowdinApiClient/Model/AiFineTuningJob.php create mode 100644 src/CrowdinApiClient/Model/AiPrompt.php create mode 100644 src/CrowdinApiClient/Model/AiPromptCompletion.php create mode 100644 src/CrowdinApiClient/Model/AiProvider.php create mode 100644 src/CrowdinApiClient/Model/AiProviderModel.php create mode 100644 src/CrowdinApiClient/Model/AiProxyChatCompletion.php create mode 100644 src/CrowdinApiClient/Model/AiReport.php create mode 100644 src/CrowdinApiClient/Model/AiSettings.php create mode 100644 src/CrowdinApiClient/Model/AiSnippet.php diff --git a/src/CrowdinApiClient/Api/AiApi.php b/src/CrowdinApiClient/Api/AiApi.php index ee40fcae..07967f14 100644 --- a/src/CrowdinApiClient/Api/AiApi.php +++ b/src/CrowdinApiClient/Api/AiApi.php @@ -2,12 +2,25 @@ namespace CrowdinApiClient\Api; +use CrowdinApiClient\Model\AiCustomPlaceholder; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiPrompt; +use CrowdinApiClient\Model\AiPromptCompletion; +use CrowdinApiClient\Model\AiProvider; +use CrowdinApiClient\Model\AiProviderModel; +use CrowdinApiClient\Model\AiProxyChatCompletion; +use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiSettings; +use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\ModelCollection; /** - * Use API to leverage AI-powered translation of strings. + * Use API to manage AI providers, prompts, and leverage AI-powered translation. * * @package Crowdin\Api */ @@ -118,4 +131,637 @@ public function downloadFileTranslationStrings(int $userId, string $jobIdentifie $path = sprintf('users/%d/ai/file-translations/%s/translations', $userId, $jobIdentifier); return $this->_get($path, DownloadFile::class); } + + /** + * List AI Prompts + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.getMany API Documentation + * + * @param int $userId + * @param array $params + * integer $params[projectId]
+ * string $params[action]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listPrompts(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/prompts', $userId); + return $this->_list($path, AiPrompt::class, $params); + } + + /** + * Add AI Prompt + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.post API Documentation + * + * @param int $userId + * @param array $data + * string $data[name] required
+ * string $data[action] required
+ * array $data[config] required
+ * integer $data[aiProviderId]
+ * string $data[aiModelId]
+ * boolean $data[isEnabled]
+ * array $data[enabledProjectIds] + * @return AiPrompt|null + */ + public function createPrompt(int $userId, array $data): ?AiPrompt + { + $path = sprintf('users/%d/ai/prompts', $userId); + return $this->_create($path, AiPrompt::class, $data); + } + + /** + * Get AI Prompt + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.get API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @return AiPrompt|null + */ + public function getPrompt(int $userId, int $aiPromptId): ?AiPrompt + { + $path = sprintf('users/%d/ai/prompts/%d', $userId, $aiPromptId); + return $this->_get($path, AiPrompt::class); + } + + /** + * Edit AI Prompt + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.patch API Documentation + * + * @param int $userId + * @param AiPrompt $aiPrompt + * @return AiPrompt|null + */ + public function updatePrompt(int $userId, AiPrompt $aiPrompt): ?AiPrompt + { + $path = sprintf('users/%d/ai/prompts/%d', $userId, $aiPrompt->getId()); + return $this->_update($path, $aiPrompt); + } + + /** + * Delete AI Prompt + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.delete API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @return mixed + */ + public function deletePrompt(int $userId, int $aiPromptId) + { + $path = sprintf('users/%d/ai/prompts/%d', $userId, $aiPromptId); + return $this->_delete($path); + } + + /** + * Clone AI Prompt + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.clones.post API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param array $data + * string $data[name] required + * @return AiPrompt|null + */ + public function clonePrompt(int $userId, int $aiPromptId, array $data): ?AiPrompt + { + $path = sprintf('users/%d/ai/prompts/%d/clones', $userId, $aiPromptId); + return $this->_post($path, AiPrompt::class, $data); + } + + /** + * Create AI Prompt Completion + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.completions.post API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param array $data + * array $data[resources] required
+ * array $data[tools]
+ * mixed $data[tool_choice] + * @return AiPromptCompletion|null + */ + public function createPromptCompletion(int $userId, int $aiPromptId, array $data): ?AiPromptCompletion + { + $path = sprintf('users/%d/ai/prompts/%d/completions', $userId, $aiPromptId); + return $this->_post($path, AiPromptCompletion::class, $data); + } + + /** + * Get AI Prompt Completion Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.completions.get API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $completionId + * @return AiPromptCompletion|null + */ + public function getPromptCompletion(int $userId, int $aiPromptId, string $completionId): ?AiPromptCompletion + { + $path = sprintf('users/%d/ai/prompts/%d/completions/%s', $userId, $aiPromptId, $completionId); + return $this->_get($path, AiPromptCompletion::class); + } + + /** + * Cancel AI Prompt Completion + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.completions.delete API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $completionId + * @return mixed + */ + public function deletePromptCompletion(int $userId, int $aiPromptId, string $completionId) + { + $path = sprintf('users/%d/ai/prompts/%d/completions/%s', $userId, $aiPromptId, $completionId); + return $this->_delete($path); + } + + /** + * Download AI Prompt Completion + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.completions.download.download API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $completionId + * @return DownloadFile|null + */ + public function downloadPromptCompletion(int $userId, int $aiPromptId, string $completionId): ?DownloadFile + { + $path = sprintf('users/%d/ai/prompts/%d/completions/%s/download', $userId, $aiPromptId, $completionId); + return $this->_get($path, DownloadFile::class); + } + + /** + * Generate AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.post API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param array $data + * array $data[projectIds]
+ * array $data[tmIds]
+ * string $data[purpose]
+ * string $data[dateFrom]
+ * string $data[dateTo]
+ * integer $data[maxFileSize]
+ * integer $data[minExamplesCount]
+ * integer $data[maxExamplesCount] + * @return AiFineTuningDataset|null + */ + public function createFineTuningDataset(int $userId, int $aiPromptId, array $data): ?AiFineTuningDataset + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/datasets', $userId, $aiPromptId); + return $this->_post($path, AiFineTuningDataset::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Dataset Generation Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.get API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningDataset|null + */ + public function getFineTuningDataset(int $userId, int $aiPromptId, string $jobIdentifier): ?AiFineTuningDataset + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/datasets/%s', $userId, $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningDataset::class); + } + + /** + * Download AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.datasets.download.get API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return DownloadFile|null + */ + public function downloadFineTuningDataset(int $userId, int $aiPromptId, string $jobIdentifier): ?DownloadFile + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/datasets/%s/download', $userId, $aiPromptId, $jobIdentifier); + return $this->_get($path, DownloadFile::class); + } + + /** + * Create AI Prompt Fine-Tuning Job + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.jobs.post API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param array $data + * array $data[trainingOptions] required
+ * boolean $data[dryRun]
+ * array $data[hyperparameters]
+ * array $data[validationOptions] + * @return AiFineTuningJob|null + */ + public function createFineTuningJob(int $userId, int $aiPromptId, array $data): ?AiFineTuningJob + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/jobs', $userId, $aiPromptId); + return $this->_post($path, AiFineTuningJob::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Job Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.jobs.get API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningJob|null + */ + public function getFineTuningJob(int $userId, int $aiPromptId, string $jobIdentifier): ?AiFineTuningJob + { + $path = sprintf('users/%d/ai/prompts/%d/fine-tuning/jobs/%s', $userId, $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningJob::class); + } + + /** + * List AI Prompt Fine-Tuning Jobs + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.jobs.getMany API Documentation + * + * @param int $userId + * @param array $params + * string $params[statuses]
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listFineTuningJobs(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/prompts/fine-tuning/jobs', $userId); + return $this->_list($path, AiFineTuningJob::class, $params); + } + + /** + * List AI Prompt Fine-Tuning Job Events + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.fine-tuning.jobs.events.getMany API Documentation + * + * @param int $userId + * @param int $aiPromptId + * @param string $jobIdentifier + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listFineTuningJobEvents( + int $userId, + int $aiPromptId, + string $jobIdentifier, + array $params = [] + ): ModelCollection { + $path = sprintf( + 'users/%d/ai/prompts/%d/fine-tuning/jobs/%s/events', + $userId, + $aiPromptId, + $jobIdentifier + ); + return $this->_list($path, AiFineTuningEvent::class, $params); + } + + /** + * List AI Providers + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.getMany API Documentation + * + * @param int $userId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProviders(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/providers', $userId); + return $this->_list($path, AiProvider::class, $params); + } + + /** + * Add AI Provider + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.post API Documentation + * + * @param int $userId + * @param array $data + * string $data[name] required
+ * string $data[type] required
+ * array $data[credentials]
+ * array $data[config]
+ * boolean $data[isEnabled]
+ * boolean $data[useSystemCredentials] + * @return AiProvider|null + */ + public function createProvider(int $userId, array $data): ?AiProvider + { + $path = sprintf('users/%d/ai/providers', $userId); + return $this->_create($path, AiProvider::class, $data); + } + + /** + * Get AI Provider + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.get API Documentation + * + * @param int $userId + * @param int $aiProviderId + * @return AiProvider|null + */ + public function getProvider(int $userId, int $aiProviderId): ?AiProvider + { + $path = sprintf('users/%d/ai/providers/%d', $userId, $aiProviderId); + return $this->_get($path, AiProvider::class); + } + + /** + * Edit AI Provider + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.patch API Documentation + * + * @param int $userId + * @param AiProvider $aiProvider + * @return AiProvider|null + */ + public function updateProvider(int $userId, AiProvider $aiProvider): ?AiProvider + { + $path = sprintf('users/%d/ai/providers/%d', $userId, $aiProvider->getId()); + return $this->_update($path, $aiProvider); + } + + /** + * Delete AI Provider + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.delete API Documentation + * + * @param int $userId + * @param int $aiProviderId + * @return mixed + */ + public function deleteProvider(int $userId, int $aiProviderId) + { + $path = sprintf('users/%d/ai/providers/%d', $userId, $aiProviderId); + return $this->_delete($path); + } + + /** + * List AI Provider Models + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.models.getMany API Documentation + * + * @param int $userId + * @param int $aiProviderId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProviderModels(int $userId, int $aiProviderId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/providers/%d/models', $userId, $aiProviderId); + return $this->_list($path, AiProviderModel::class, $params); + } + + /** + * Create AI Provider Chat Completion + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.chat.completions.post API Documentation + * + * @param int $userId + * @param int $aiProviderId + * @param array $data + * string $data[model]
+ * boolean $data[stream] + * @return AiProxyChatCompletion|null + */ + public function createProviderChatCompletion( + int $userId, + int $aiProviderId, + array $data + ): ?AiProxyChatCompletion { + $path = sprintf('users/%d/ai/providers/%d/chat/completions', $userId, $aiProviderId); + return $this->_post($path, AiProxyChatCompletion::class, $data); + } + + /** + * Generate AI Report + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.reports.post API Documentation + * + * @param int $userId + * @param array $data + * string $data[type] required
+ * array $data[schema] required + * @return AiReport|null + */ + public function generateReport(int $userId, array $data): ?AiReport + { + $path = sprintf('users/%d/ai/reports', $userId); + return $this->_post($path, AiReport::class, $data); + } + + /** + * Check AI Report Generation Status + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.reports.get API Documentation + * + * @param int $userId + * @param string $aiReportId + * @return AiReport|null + */ + public function getReport(int $userId, string $aiReportId): ?AiReport + { + $path = sprintf('users/%d/ai/reports/%s', $userId, $aiReportId); + return $this->_get($path, AiReport::class); + } + + /** + * Download AI Report + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.reports.download.download API Documentation + * + * @param int $userId + * @param string $aiReportId + * @return DownloadFile|null + */ + public function downloadReport(int $userId, string $aiReportId): ?DownloadFile + { + $path = sprintf('users/%d/ai/reports/%s/download', $userId, $aiReportId); + return $this->_get($path, DownloadFile::class); + } + + /** + * Get AI Settings + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.get API Documentation + * + * @param int $userId + * @return AiSettings|null + */ + public function getSettings(int $userId): ?AiSettings + { + $path = sprintf('users/%d/ai/settings', $userId); + return $this->_get($path, AiSettings::class); + } + + /** + * Edit AI Settings + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.patch API Documentation + * + * @param int $userId + * @param AiSettings $aiSettings + * @return AiSettings|null + */ + public function updateSettings(int $userId, AiSettings $aiSettings): ?AiSettings + { + $path = sprintf('users/%d/ai/settings', $userId); + return $this->_update($path, $aiSettings); + } + + /** + * List AI Custom Placeholders + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.custom-placeholders.getMany API Documentation + * @deprecated Use listSnippets instead + * + * @param int $userId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listCustomPlaceholders(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/settings/custom-placeholders', $userId); + return $this->_list($path, AiCustomPlaceholder::class, $params); + } + + /** + * Add AI Custom Placeholder + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.custom-placeholders.post API Documentation + * @deprecated Use createSnippet instead + * + * @param int $userId + * @param array $data + * string $data[description] required
+ * string $data[placeholder] required
+ * string $data[value] required + * @return AiCustomPlaceholder|null + */ + public function createCustomPlaceholder(int $userId, array $data): ?AiCustomPlaceholder + { + $path = sprintf('users/%d/ai/settings/custom-placeholders', $userId); + return $this->_create($path, AiCustomPlaceholder::class, $data); + } + + /** + * Get AI Custom Placeholder + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.custom-placeholders.get API Documentation + * @deprecated Use getSnippet instead + * + * @param int $userId + * @param int $aiCustomPlaceholderId + * @return AiCustomPlaceholder|null + */ + public function getCustomPlaceholder(int $userId, int $aiCustomPlaceholderId): ?AiCustomPlaceholder + { + $path = sprintf('users/%d/ai/settings/custom-placeholders/%d', $userId, $aiCustomPlaceholderId); + return $this->_get($path, AiCustomPlaceholder::class); + } + + /** + * Edit AI Custom Placeholder + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.custom-placeholders.patch API Documentation + * @deprecated Use updateSnippet instead + * + * @param int $userId + * @param AiCustomPlaceholder $aiCustomPlaceholder + * @return AiCustomPlaceholder|null + */ + public function updateCustomPlaceholder(int $userId, AiCustomPlaceholder $aiCustomPlaceholder): ?AiCustomPlaceholder + { + $path = sprintf('users/%d/ai/settings/custom-placeholders/%d', $userId, $aiCustomPlaceholder->getId()); + return $this->_update($path, $aiCustomPlaceholder); + } + + /** + * Delete AI Custom Placeholder + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.custom-placeholders.delete API Documentation + * @deprecated Use deleteSnippet instead + * + * @param int $userId + * @param int $aiCustomPlaceholderId + * @return mixed + */ + public function deleteCustomPlaceholder(int $userId, int $aiCustomPlaceholderId) + { + $path = sprintf('users/%d/ai/settings/custom-placeholders/%d', $userId, $aiCustomPlaceholderId); + return $this->_delete($path); + } + + /** + * List AI Snippets + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.getMany API Documentation + * + * @param int $userId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listSnippets(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/settings/snippets', $userId); + return $this->_list($path, AiSnippet::class, $params); + } + + /** + * Add AI Snippet + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.post API Documentation + * + * @param int $userId + * @param array $data + * string $data[description] required
+ * string $data[placeholder] required
+ * string $data[value] required + * @return AiSnippet|null + */ + public function createSnippet(int $userId, array $data): ?AiSnippet + { + $path = sprintf('users/%d/ai/settings/snippets', $userId); + return $this->_create($path, AiSnippet::class, $data); + } + + /** + * Get AI Snippet + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.get API Documentation + * + * @param int $userId + * @param int $aiSnippetId + * @return AiSnippet|null + */ + public function getSnippet(int $userId, int $aiSnippetId): ?AiSnippet + { + $path = sprintf('users/%d/ai/settings/snippets/%d', $userId, $aiSnippetId); + return $this->_get($path, AiSnippet::class); + } + + /** + * Edit AI Snippet + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.patch API Documentation + * + * @param int $userId + * @param AiSnippet $aiSnippet + * @return AiSnippet|null + */ + public function updateSnippet(int $userId, AiSnippet $aiSnippet): ?AiSnippet + { + $path = sprintf('users/%d/ai/settings/snippets/%d', $userId, $aiSnippet->getId()); + return $this->_update($path, $aiSnippet); + } + + /** + * Delete AI Snippet + * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.snippets.delete API Documentation + * + * @param int $userId + * @param int $aiSnippetId + * @return mixed + */ + public function deleteSnippet(int $userId, int $aiSnippetId) + { + $path = sprintf('users/%d/ai/settings/snippets/%d', $userId, $aiSnippetId); + return $this->_delete($path); + } } diff --git a/src/CrowdinApiClient/Api/Enterprise/AiApi.php b/src/CrowdinApiClient/Api/Enterprise/AiApi.php index 0eeb0344..c230da4f 100644 --- a/src/CrowdinApiClient/Api/Enterprise/AiApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/AiApi.php @@ -3,12 +3,25 @@ namespace CrowdinApiClient\Api\Enterprise; use CrowdinApiClient\Api\AbstractApi; +use CrowdinApiClient\Model\AiCustomPlaceholder; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiPrompt; +use CrowdinApiClient\Model\AiPromptCompletion; +use CrowdinApiClient\Model\AiProvider; +use CrowdinApiClient\Model\AiProviderModel; +use CrowdinApiClient\Model\AiProxyChatCompletion; +use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiSettings; +use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\ModelCollection; /** - * Use API to leverage AI-powered translation of strings. + * Use API to manage AI providers, prompts, and leverage AI-powered translation. * * @package Crowdin\Api\Enterprise */ @@ -111,4 +124,577 @@ public function downloadFileTranslationStrings(string $jobIdentifier): ?Download $path = sprintf('ai/file-translations/%s/translations', $jobIdentifier); return $this->_get($path, DownloadFile::class); } + + /** + * List AI Prompts + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.getMany API Documentation + * + * @param array $params + * integer $params[projectId]
+ * string $params[action]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listPrompts(array $params = []): ModelCollection + { + return $this->_list('ai/prompts', AiPrompt::class, $params); + } + + /** + * Add AI Prompt + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.post API Documentation + * + * @param array $data + * string $data[name] required
+ * string $data[action] required
+ * array $data[config] required
+ * integer $data[aiProviderId]
+ * string $data[aiModelId]
+ * boolean $data[isEnabled]
+ * array $data[enabledProjectIds] + * @return AiPrompt|null + */ + public function createPrompt(array $data): ?AiPrompt + { + return $this->_create('ai/prompts', AiPrompt::class, $data); + } + + /** + * Get AI Prompt + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.get API Documentation + * + * @param int $aiPromptId + * @return AiPrompt|null + */ + public function getPrompt(int $aiPromptId): ?AiPrompt + { + $path = sprintf('ai/prompts/%d', $aiPromptId); + return $this->_get($path, AiPrompt::class); + } + + /** + * Edit AI Prompt + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.patch API Documentation + * + * @param AiPrompt $aiPrompt + * @return AiPrompt|null + */ + public function updatePrompt(AiPrompt $aiPrompt): ?AiPrompt + { + $path = sprintf('ai/prompts/%d', $aiPrompt->getId()); + return $this->_update($path, $aiPrompt); + } + + /** + * Delete AI Prompt + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.delete API Documentation + * + * @param int $aiPromptId + * @return mixed + */ + public function deletePrompt(int $aiPromptId) + { + $path = sprintf('ai/prompts/%d', $aiPromptId); + return $this->_delete($path); + } + + /** + * Clone AI Prompt + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.clones.post API Documentation + * + * @param int $aiPromptId + * @param array $data + * string $data[name] required + * @return AiPrompt|null + */ + public function clonePrompt(int $aiPromptId, array $data): ?AiPrompt + { + $path = sprintf('ai/prompts/%d/clones', $aiPromptId); + return $this->_post($path, AiPrompt::class, $data); + } + + /** + * Create AI Prompt Completion + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.completions.post API Documentation + * + * @param int $aiPromptId + * @param array $data + * array $data[resources] required
+ * array $data[tools]
+ * mixed $data[tool_choice] + * @return AiPromptCompletion|null + */ + public function createPromptCompletion(int $aiPromptId, array $data): ?AiPromptCompletion + { + $path = sprintf('ai/prompts/%d/completions', $aiPromptId); + return $this->_post($path, AiPromptCompletion::class, $data); + } + + /** + * Get AI Prompt Completion Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.completions.get API Documentation + * + * @param int $aiPromptId + * @param string $completionId + * @return AiPromptCompletion|null + */ + public function getPromptCompletion(int $aiPromptId, string $completionId): ?AiPromptCompletion + { + $path = sprintf('ai/prompts/%d/completions/%s', $aiPromptId, $completionId); + return $this->_get($path, AiPromptCompletion::class); + } + + /** + * Cancel AI Prompt Completion + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.completions.delete API Documentation + * + * @param int $aiPromptId + * @param string $completionId + * @return mixed + */ + public function deletePromptCompletion(int $aiPromptId, string $completionId) + { + $path = sprintf('ai/prompts/%d/completions/%s', $aiPromptId, $completionId); + return $this->_delete($path); + } + + /** + * Download AI Prompt Completion + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.completions.download.download API Documentation + * + * @param int $aiPromptId + * @param string $completionId + * @return DownloadFile|null + */ + public function downloadPromptCompletion(int $aiPromptId, string $completionId): ?DownloadFile + { + $path = sprintf('ai/prompts/%d/completions/%s/download', $aiPromptId, $completionId); + return $this->_get($path, DownloadFile::class); + } + + /** + * Generate AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.post API Documentation + * + * @param int $aiPromptId + * @param array $data + * array $data[projectIds]
+ * array $data[tmIds]
+ * string $data[purpose]
+ * string $data[dateFrom]
+ * string $data[dateTo]
+ * integer $data[maxFileSize]
+ * integer $data[minExamplesCount]
+ * integer $data[maxExamplesCount] + * @return AiFineTuningDataset|null + */ + public function createFineTuningDataset(int $aiPromptId, array $data): ?AiFineTuningDataset + { + $path = sprintf('ai/prompts/%d/fine-tuning/datasets', $aiPromptId); + return $this->_post($path, AiFineTuningDataset::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Dataset Generation Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.get API Documentation + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningDataset|null + */ + public function getFineTuningDataset(int $aiPromptId, string $jobIdentifier): ?AiFineTuningDataset + { + $path = sprintf('ai/prompts/%d/fine-tuning/datasets/%s', $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningDataset::class); + } + + /** + * Download AI Prompt Fine-Tuning Dataset + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.datasets.download.get API Documentation + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return DownloadFile|null + */ + public function downloadFineTuningDataset(int $aiPromptId, string $jobIdentifier): ?DownloadFile + { + $path = sprintf('ai/prompts/%d/fine-tuning/datasets/%s/download', $aiPromptId, $jobIdentifier); + return $this->_get($path, DownloadFile::class); + } + + /** + * Create AI Prompt Fine-Tuning Job + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.post API Documentation + * + * @param int $aiPromptId + * @param array $data + * array $data[trainingOptions] required
+ * boolean $data[dryRun]
+ * array $data[hyperparameters]
+ * array $data[validationOptions] + * @return AiFineTuningJob|null + */ + public function createFineTuningJob(int $aiPromptId, array $data): ?AiFineTuningJob + { + $path = sprintf('ai/prompts/%d/fine-tuning/jobs', $aiPromptId); + return $this->_post($path, AiFineTuningJob::class, $data); + } + + /** + * Get AI Prompt Fine-Tuning Job Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.get API Documentation + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @return AiFineTuningJob|null + */ + public function getFineTuningJob(int $aiPromptId, string $jobIdentifier): ?AiFineTuningJob + { + $path = sprintf('ai/prompts/%d/fine-tuning/jobs/%s', $aiPromptId, $jobIdentifier); + return $this->_get($path, AiFineTuningJob::class); + } + + /** + * List AI Prompt Fine-Tuning Jobs + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.getMany API Documentation + * + * @param array $params + * string $params[statuses]
+ * string $params[orderBy]
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listFineTuningJobs(array $params = []): ModelCollection + { + return $this->_list('ai/prompts/fine-tuning/jobs', AiFineTuningJob::class, $params); + } + + /** + * List AI Prompt Fine-Tuning Job Events + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.fine-tuning.jobs.events.getMany API Documentation + * + * @param int $aiPromptId + * @param string $jobIdentifier + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listFineTuningJobEvents( + int $aiPromptId, + string $jobIdentifier, + array $params = [] + ): ModelCollection { + $path = sprintf('ai/prompts/%d/fine-tuning/jobs/%s/events', $aiPromptId, $jobIdentifier); + return $this->_list($path, AiFineTuningEvent::class, $params); + } + + /** + * List AI Providers + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.getMany API Documentation + * + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProviders(array $params = []): ModelCollection + { + return $this->_list('ai/providers', AiProvider::class, $params); + } + + /** + * Add AI Provider + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.post API Documentation + * + * @param array $data + * string $data[name] required
+ * string $data[type] required
+ * array $data[credentials]
+ * array $data[config]
+ * boolean $data[isEnabled]
+ * boolean $data[useSystemCredentials] + * @return AiProvider|null + */ + public function createProvider(array $data): ?AiProvider + { + return $this->_create('ai/providers', AiProvider::class, $data); + } + + /** + * Get AI Provider + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.get API Documentation + * + * @param int $aiProviderId + * @return AiProvider|null + */ + public function getProvider(int $aiProviderId): ?AiProvider + { + $path = sprintf('ai/providers/%d', $aiProviderId); + return $this->_get($path, AiProvider::class); + } + + /** + * Edit AI Provider + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.patch API Documentation + * + * @param AiProvider $aiProvider + * @return AiProvider|null + */ + public function updateProvider(AiProvider $aiProvider): ?AiProvider + { + $path = sprintf('ai/providers/%d', $aiProvider->getId()); + return $this->_update($path, $aiProvider); + } + + /** + * Delete AI Provider + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.delete API Documentation + * + * @param int $aiProviderId + * @return mixed + */ + public function deleteProvider(int $aiProviderId) + { + $path = sprintf('ai/providers/%d', $aiProviderId); + return $this->_delete($path); + } + + /** + * List AI Provider Models + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.models.getMany API Documentation + * + * @param int $aiProviderId + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listProviderModels(int $aiProviderId, array $params = []): ModelCollection + { + $path = sprintf('ai/providers/%d/models', $aiProviderId); + return $this->_list($path, AiProviderModel::class, $params); + } + + /** + * Create AI Provider Chat Completion + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.chat.completions.post API Documentation + * + * @param int $aiProviderId + * @param array $data + * string $data[model]
+ * boolean $data[stream] + * @return AiProxyChatCompletion|null + */ + public function createProviderChatCompletion(int $aiProviderId, array $data): ?AiProxyChatCompletion + { + $path = sprintf('ai/providers/%d/chat/completions', $aiProviderId); + return $this->_post($path, AiProxyChatCompletion::class, $data); + } + + /** + * Generate AI Report + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.reports.post API Documentation + * + * @param array $data + * string $data[type] required
+ * array $data[schema] required + * @return AiReport|null + */ + public function generateReport(array $data): ?AiReport + { + return $this->_post('ai/reports', AiReport::class, $data); + } + + /** + * Check AI Report Generation Status + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.reports.get API Documentation + * + * @param string $aiReportId + * @return AiReport|null + */ + public function getReport(string $aiReportId): ?AiReport + { + $path = sprintf('ai/reports/%s', $aiReportId); + return $this->_get($path, AiReport::class); + } + + /** + * Download AI Report + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.reports.download.download API Documentation + * + * @param string $aiReportId + * @return DownloadFile|null + */ + public function downloadReport(string $aiReportId): ?DownloadFile + { + $path = sprintf('ai/reports/%s/download', $aiReportId); + return $this->_get($path, DownloadFile::class); + } + + /** + * Get AI Settings + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.get API Documentation + * + * @return AiSettings|null + */ + public function getSettings(): ?AiSettings + { + return $this->_get('ai/settings', AiSettings::class); + } + + /** + * Edit AI Settings + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.patch API Documentation + * + * @param AiSettings $aiSettings + * @return AiSettings|null + */ + public function updateSettings(AiSettings $aiSettings): ?AiSettings + { + return $this->_update('ai/settings', $aiSettings); + } + + /** + * List AI Custom Placeholders + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.custom-placeholders.getMany API Documentation + * @deprecated Use listSnippets instead + * + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listCustomPlaceholders(array $params = []): ModelCollection + { + return $this->_list('ai/settings/custom-placeholders', AiCustomPlaceholder::class, $params); + } + + /** + * Add AI Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.custom-placeholders.post API Documentation + * @deprecated Use createSnippet instead + * + * @param array $data + * string $data[description] required
+ * string $data[placeholder] required
+ * string $data[value] required + * @return AiCustomPlaceholder|null + */ + public function createCustomPlaceholder(array $data): ?AiCustomPlaceholder + { + return $this->_create('ai/settings/custom-placeholders', AiCustomPlaceholder::class, $data); + } + + /** + * Get AI Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.custom-placeholders.get API Documentation + * @deprecated Use getSnippet instead + * + * @param int $aiCustomPlaceholderId + * @return AiCustomPlaceholder|null + */ + public function getCustomPlaceholder(int $aiCustomPlaceholderId): ?AiCustomPlaceholder + { + $path = sprintf('ai/settings/custom-placeholders/%d', $aiCustomPlaceholderId); + return $this->_get($path, AiCustomPlaceholder::class); + } + + /** + * Edit AI Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.custom-placeholders.patch API Documentation + * @deprecated Use updateSnippet instead + * + * @param AiCustomPlaceholder $aiCustomPlaceholder + * @return AiCustomPlaceholder|null + */ + public function updateCustomPlaceholder(AiCustomPlaceholder $aiCustomPlaceholder): ?AiCustomPlaceholder + { + $path = sprintf('ai/settings/custom-placeholders/%d', $aiCustomPlaceholder->getId()); + return $this->_update($path, $aiCustomPlaceholder); + } + + /** + * Delete AI Custom Placeholder + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.custom-placeholders.delete API Documentation + * @deprecated Use deleteSnippet instead + * + * @param int $aiCustomPlaceholderId + * @return mixed + */ + public function deleteCustomPlaceholder(int $aiCustomPlaceholderId) + { + $path = sprintf('ai/settings/custom-placeholders/%d', $aiCustomPlaceholderId); + return $this->_delete($path); + } + + /** + * List AI Snippets + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.getMany API Documentation + * + * @param array $params + * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listSnippets(array $params = []): ModelCollection + { + return $this->_list('ai/settings/snippets', AiSnippet::class, $params); + } + + /** + * Add AI Snippet + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.post API Documentation + * + * @param array $data + * string $data[description] required
+ * string $data[placeholder] required
+ * string $data[value] required + * @return AiSnippet|null + */ + public function createSnippet(array $data): ?AiSnippet + { + return $this->_create('ai/settings/snippets', AiSnippet::class, $data); + } + + /** + * Get AI Snippet + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.get API Documentation + * + * @param int $aiSnippetId + * @return AiSnippet|null + */ + public function getSnippet(int $aiSnippetId): ?AiSnippet + { + $path = sprintf('ai/settings/snippets/%d', $aiSnippetId); + return $this->_get($path, AiSnippet::class); + } + + /** + * Edit AI Snippet + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.patch API Documentation + * + * @param AiSnippet $aiSnippet + * @return AiSnippet|null + */ + public function updateSnippet(AiSnippet $aiSnippet): ?AiSnippet + { + $path = sprintf('ai/settings/snippets/%d', $aiSnippet->getId()); + return $this->_update($path, $aiSnippet); + } + + /** + * Delete AI Snippet + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.snippets.delete API Documentation + * + * @param int $aiSnippetId + * @return mixed + */ + public function deleteSnippet(int $aiSnippetId) + { + $path = sprintf('ai/settings/snippets/%d', $aiSnippetId); + return $this->_delete($path); + } } diff --git a/src/CrowdinApiClient/Model/AiCustomPlaceholder.php b/src/CrowdinApiClient/Model/AiCustomPlaceholder.php new file mode 100644 index 00000000..b7d8d3e0 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiCustomPlaceholder.php @@ -0,0 +1,86 @@ +id = (int)$this->getDataProperty('id'); + $this->description = (string)$this->getDataProperty('description'); + $this->placeholder = (string)$this->getDataProperty('placeholder'); + $this->value = (string)$this->getDataProperty('value'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + } + + public function getId(): int + { + return $this->id; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getPlaceholder(): string + { + return $this->placeholder; + } + + public function getValue(): string + { + return $this->value; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): string + { + return $this->updatedAt; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function setPlaceholder(string $placeholder): void + { + $this->placeholder = $placeholder; + } + + public function setValue(string $value): void + { + $this->value = $value; + } +} diff --git a/src/CrowdinApiClient/Model/AiFineTuningDataset.php b/src/CrowdinApiClient/Model/AiFineTuningDataset.php new file mode 100644 index 00000000..4ba02d39 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiFineTuningDataset.php @@ -0,0 +1,86 @@ +identifier = (string)$this->getDataProperty('identifier'); + $this->status = (string)$this->getDataProperty('status'); + $this->progress = (int)$this->getDataProperty('progress'); + $this->attributes = (array)$this->getDataProperty('attributes'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = $this->getDataProperty('updatedAt'); + $this->startedAt = $this->getDataProperty('startedAt'); + $this->finishedAt = $this->getDataProperty('finishedAt'); + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getProgress(): int + { + return $this->progress; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } + + public function getStartedAt(): ?string + { + return $this->startedAt; + } + + public function getFinishedAt(): ?string + { + return $this->finishedAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiFineTuningEvent.php b/src/CrowdinApiClient/Model/AiFineTuningEvent.php new file mode 100644 index 00000000..abd112af --- /dev/null +++ b/src/CrowdinApiClient/Model/AiFineTuningEvent.php @@ -0,0 +1,59 @@ +id = (string)$this->getDataProperty('id'); + $this->type = (string)$this->getDataProperty('type'); + $this->message = (string)$this->getDataProperty('message'); + $this->data = $this->getDataProperty('data'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + } + + public function getId(): string + { + return $this->id; + } + + public function getType(): string + { + return $this->type; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getMetrics(): ?array + { + return $this->data; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiFineTuningJob.php b/src/CrowdinApiClient/Model/AiFineTuningJob.php new file mode 100644 index 00000000..44427730 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiFineTuningJob.php @@ -0,0 +1,86 @@ +identifier = (string)$this->getDataProperty('identifier'); + $this->status = (string)$this->getDataProperty('status'); + $this->progress = (int)$this->getDataProperty('progress'); + $this->attributes = (array)$this->getDataProperty('attributes'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = $this->getDataProperty('updatedAt'); + $this->startedAt = $this->getDataProperty('startedAt'); + $this->finishedAt = $this->getDataProperty('finishedAt'); + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getProgress(): int + { + return $this->progress; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } + + public function getStartedAt(): ?string + { + return $this->startedAt; + } + + public function getFinishedAt(): ?string + { + return $this->finishedAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiPrompt.php b/src/CrowdinApiClient/Model/AiPrompt.php new file mode 100644 index 00000000..ce9fa8d6 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiPrompt.php @@ -0,0 +1,197 @@ +id = (int)$this->getDataProperty('id'); + $this->name = (string)$this->getDataProperty('name'); + $this->action = (string)$this->getDataProperty('action'); + $this->aiProviderId = $this->getDataProperty('aiProviderId'); + $this->aiModelId = $this->getDataProperty('aiModelId'); + $this->isEnabled = (bool)$this->getDataProperty('isEnabled'); + $this->enabledProjectIds = (array)$this->getDataProperty('enabledProjectIds'); + $this->config = (array)$this->getDataProperty('config'); + $this->promptPreview = $this->getDataProperty('promptPreview'); + $this->isFineTuningAvailable = (bool)$this->getDataProperty('isFineTuningAvailable'); + $this->createdBy = $this->getDataProperty('createdBy'); + $this->updatedBy = $this->getDataProperty('updatedBy'); + $this->lastUsedBy = $this->getDataProperty('lastUsedBy'); + $this->lastUsedAt = $this->getDataProperty('lastUsedAt'); + $this->usageCount = (int)$this->getDataProperty('usageCount'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getAction(): string + { + return $this->action; + } + + public function getAiProviderId(): ?int + { + return $this->aiProviderId; + } + + public function getAiModelId(): ?string + { + return $this->aiModelId; + } + + public function isEnabled(): bool + { + return $this->isEnabled; + } + + public function getEnabledProjectIds(): array + { + return $this->enabledProjectIds; + } + + public function getConfig(): array + { + return $this->config; + } + + public function getPromptPreview(): ?string + { + return $this->promptPreview; + } + + public function isFineTuningAvailable(): bool + { + return $this->isFineTuningAvailable; + } + + public function getCreatedBy(): ?int + { + return $this->createdBy; + } + + public function getUpdatedBy(): ?int + { + return $this->updatedBy; + } + + public function getLastUsedBy(): ?int + { + return $this->lastUsedBy; + } + + public function getLastUsedAt(): ?string + { + return $this->lastUsedAt; + } + + public function getUsageCount(): int + { + return $this->usageCount; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): string + { + return $this->updatedAt; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function setAction(string $action): void + { + $this->action = $action; + } + + public function setAiProviderId(?int $aiProviderId): void + { + $this->aiProviderId = $aiProviderId; + } + + public function setAiModelId(?string $aiModelId): void + { + $this->aiModelId = $aiModelId; + } + + public function setEnabledProjectIds(array $enabledProjectIds): void + { + $this->enabledProjectIds = $enabledProjectIds; + } + + public function setConfig(array $config): void + { + $this->config = $config; + } +} diff --git a/src/CrowdinApiClient/Model/AiPromptCompletion.php b/src/CrowdinApiClient/Model/AiPromptCompletion.php new file mode 100644 index 00000000..b676ad42 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiPromptCompletion.php @@ -0,0 +1,86 @@ +identifier = (string)$this->getDataProperty('identifier'); + $this->status = (string)$this->getDataProperty('status'); + $this->progress = (int)$this->getDataProperty('progress'); + $this->attributes = (array)$this->getDataProperty('attributes'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = $this->getDataProperty('updatedAt'); + $this->startedAt = $this->getDataProperty('startedAt'); + $this->finishedAt = $this->getDataProperty('finishedAt'); + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getProgress(): int + { + return $this->progress; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } + + public function getStartedAt(): ?string + { + return $this->startedAt; + } + + public function getFinishedAt(): ?string + { + return $this->finishedAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiProvider.php b/src/CrowdinApiClient/Model/AiProvider.php new file mode 100644 index 00000000..3bb40220 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiProvider.php @@ -0,0 +1,134 @@ +id = (int)$this->getDataProperty('id'); + $this->name = (string)$this->getDataProperty('name'); + $this->type = (string)$this->getDataProperty('type'); + $this->credentials = $this->getDataProperty('credentials'); + $this->config = (array)$this->getDataProperty('config'); + $this->isEnabled = (bool)$this->getDataProperty('isEnabled'); + $this->useSystemCredentials = (bool)$this->getDataProperty('useSystemCredentials'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + $this->promptsCount = (int)$this->getDataProperty('promptsCount'); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getType(): string + { + return $this->type; + } + + public function getCredentials(): ?array + { + return $this->credentials; + } + + public function getConfig(): array + { + return $this->config; + } + + public function isEnabled(): bool + { + return $this->isEnabled; + } + + public function isUseSystemCredentials(): bool + { + return $this->useSystemCredentials; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): string + { + return $this->updatedAt; + } + + public function getPromptsCount(): int + { + return $this->promptsCount; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function setType(string $type): void + { + $this->type = $type; + } + + public function setCredentials(?array $credentials): void + { + $this->credentials = $credentials; + } + + public function setConfig(array $config): void + { + $this->config = $config; + } + + public function setIsEnabled(bool $isEnabled): void + { + $this->isEnabled = $isEnabled; + } + + public function setUseSystemCredentials(bool $useSystemCredentials): void + { + $this->useSystemCredentials = $useSystemCredentials; + } +} diff --git a/src/CrowdinApiClient/Model/AiProviderModel.php b/src/CrowdinApiClient/Model/AiProviderModel.php new file mode 100644 index 00000000..e7365b98 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiProviderModel.php @@ -0,0 +1,50 @@ +id = (string)$this->getDataProperty('id'); + $this->provider = $this->getDataProperty('provider'); + $this->providerName = $this->getDataProperty('providerName'); + $this->providerId = $this->getDataProperty('providerId'); + } + + public function getId(): string + { + return $this->id; + } + + public function getProvider(): ?string + { + return $this->provider; + } + + public function getProviderName(): ?string + { + return $this->providerName; + } + + public function getProviderId(): ?int + { + return $this->providerId; + } +} diff --git a/src/CrowdinApiClient/Model/AiProxyChatCompletion.php b/src/CrowdinApiClient/Model/AiProxyChatCompletion.php new file mode 100644 index 00000000..f156098d --- /dev/null +++ b/src/CrowdinApiClient/Model/AiProxyChatCompletion.php @@ -0,0 +1,13 @@ +identifier = (string)$this->getDataProperty('identifier'); + $this->status = (string)$this->getDataProperty('status'); + $this->progress = (int)$this->getDataProperty('progress'); + $this->attributes = (array)$this->getDataProperty('attributes'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = $this->getDataProperty('updatedAt'); + $this->startedAt = $this->getDataProperty('startedAt'); + $this->finishedAt = $this->getDataProperty('finishedAt'); + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getProgress(): int + { + return $this->progress; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?string + { + return $this->updatedAt; + } + + public function getStartedAt(): ?string + { + return $this->startedAt; + } + + public function getFinishedAt(): ?string + { + return $this->finishedAt; + } +} diff --git a/src/CrowdinApiClient/Model/AiSettings.php b/src/CrowdinApiClient/Model/AiSettings.php new file mode 100644 index 00000000..f58c2b5d --- /dev/null +++ b/src/CrowdinApiClient/Model/AiSettings.php @@ -0,0 +1,70 @@ +preTranslationAiPromptId = (int)$this->getDataProperty('preTranslationAiPromptId'); + $this->editorSuggestionAiPromptId = (int)$this->getDataProperty('editorSuggestionAiPromptId'); + $this->qaCheckActionAiPromptId = $this->getDataProperty('qaCheckActionAiPromptId'); + $this->contextReviewAiPromptId = $this->getDataProperty('contextReviewAiPromptId'); + } + + public function getPreTranslationAiPromptId(): int + { + return $this->preTranslationAiPromptId; + } + + public function getEditorSuggestionAiPromptId(): int + { + return $this->editorSuggestionAiPromptId; + } + + public function getQaCheckActionAiPromptId(): ?int + { + return $this->qaCheckActionAiPromptId; + } + + public function getContextReviewAiPromptId(): ?int + { + return $this->contextReviewAiPromptId; + } + + public function setPreTranslationAiPromptId(int $preTranslationAiPromptId): void + { + $this->preTranslationAiPromptId = $preTranslationAiPromptId; + } + + public function setEditorSuggestionAiPromptId(int $editorSuggestionAiPromptId): void + { + $this->editorSuggestionAiPromptId = $editorSuggestionAiPromptId; + } + + public function setQaCheckActionAiPromptId(?int $qaCheckActionAiPromptId): void + { + $this->qaCheckActionAiPromptId = $qaCheckActionAiPromptId; + } + + public function setContextReviewAiPromptId(?int $contextReviewAiPromptId): void + { + $this->contextReviewAiPromptId = $contextReviewAiPromptId; + } +} diff --git a/src/CrowdinApiClient/Model/AiSnippet.php b/src/CrowdinApiClient/Model/AiSnippet.php new file mode 100644 index 00000000..d0d3299a --- /dev/null +++ b/src/CrowdinApiClient/Model/AiSnippet.php @@ -0,0 +1,83 @@ +id = (int)$this->getDataProperty('id'); + $this->description = (string)$this->getDataProperty('description'); + $this->placeholder = (string)$this->getDataProperty('placeholder'); + $this->value = (string)$this->getDataProperty('value'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->updatedAt = (string)$this->getDataProperty('updatedAt'); + } + + public function getId(): int + { + return $this->id; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getPlaceholder(): string + { + return $this->placeholder; + } + + public function getValue(): string + { + return $this->value; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getUpdatedAt(): string + { + return $this->updatedAt; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function setPlaceholder(string $placeholder): void + { + $this->placeholder = $placeholder; + } + + public function setValue(string $value): void + { + $this->value = $value; + } +} diff --git a/tests/CrowdinApiClient/Api/AiApiTest.php b/tests/CrowdinApiClient/Api/AiApiTest.php index b8fc4d51..fee3cc1e 100644 --- a/tests/CrowdinApiClient/Api/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/AiApiTest.php @@ -2,9 +2,22 @@ namespace CrowdinApiClient\Tests\Api; +use CrowdinApiClient\Model\AiCustomPlaceholder; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiPrompt; +use CrowdinApiClient\Model\AiPromptCompletion; +use CrowdinApiClient\Model\AiProvider; +use CrowdinApiClient\Model\AiProviderModel; +use CrowdinApiClient\Model\AiProxyChatCompletion; +use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiSettings; +use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\ModelCollection; class AiApiTest extends AbstractTestApi { @@ -12,6 +25,7 @@ public function testTranslateStrings(): void { $params = [ 'strings' => ['Some text to translate!'], + 'sourceLanguageId' => 'en', 'targetLanguageId' => 'uk', ]; @@ -24,7 +38,8 @@ public function testTranslateStrings(): void "sourceLanguageId": "en", "targetLanguageId": "uk", "translations": [ - "Перекладений текст 1" + "Перекладений текст 1", + "Перекладений текст 2" ] } }', @@ -34,14 +49,23 @@ public function testTranslateStrings(): void $this->assertInstanceOf(AiTranslation::class, $aiTranslation); $this->assertEquals('en', $aiTranslation->getSourceLanguageId()); $this->assertEquals('uk', $aiTranslation->getTargetLanguageId()); - $this->assertEquals(['Перекладений текст 1'], $aiTranslation->getTranslations()); + $this->assertEquals(['Перекладений текст 1', 'Перекладений текст 2'], $aiTranslation->getTranslations()); } public function testCreateFileTranslation(): void { $params = [ 'storageId' => 123, + 'sourceLanguageId' => 'en', 'targetLanguageId' => 'uk', + 'type' => 'xliff', + 'parserVersion' => 1, + 'tmIds' => [123], + 'glossaryIds' => [456], + 'styleGuideIds' => [654], + 'aiPromptId' => 789, + 'instructions' => ['Keep a formal tone'], + 'attachmentIds' => [123], ]; $this->mockRequest([ @@ -54,9 +78,9 @@ public function testCreateFileTranslation(): void "status": "finished", "progress": 100, "attributes": { - "stage": "done", + "stage": "translate", "error": null, - "downloadName": "Sample_Chrome.json", + "downloadName": "file.pdf", "sourceLanguageId": "en", "targetLanguageId": "uk", "originalFileName": "Sample_Chrome.json", @@ -87,22 +111,22 @@ public function testGetFileTranslation(): void 'response' => '{ "data": { "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "in_progress", - "progress": 50, + "status": "finished", + "progress": 100, "attributes": { "stage": "translate", "error": null, - "downloadName": null, + "downloadName": "file.pdf", "sourceLanguageId": "en", "targetLanguageId": "uk", "originalFileName": "Sample_Chrome.json", - "detectedType": null, - "parserVersion": null + "detectedType": "chrome", + "parserVersion": 2 }, "createdAt": "2026-01-23T11:26:54+00:00", "updatedAt": "2026-01-23T11:26:54+00:00", "startedAt": "2026-01-23T11:26:54+00:00", - "finishedAt": null + "finishedAt": "2026-01-23T11:26:54+00:00" } }', ]); @@ -110,8 +134,8 @@ public function testGetFileTranslation(): void $fileTranslation = $this->crowdin->ai->getFileTranslation(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); $this->assertInstanceOf(AiFileTranslation::class, $fileTranslation); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); - $this->assertEquals('in_progress', $fileTranslation->getStatus()); - $this->assertEquals(50, $fileTranslation->getProgress()); + $this->assertEquals('finished', $fileTranslation->getStatus()); + $this->assertEquals(100, $fileTranslation->getProgress()); } public function testDeleteFileTranslation(): void @@ -132,16 +156,16 @@ public function testDownloadFileTranslation(): void 'method' => 'get', 'response' => '{ "data": { - "url": "https://example.com/download/file.json", - "expireIn": "2026-01-23T12:00:00+00:00" + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" } }', ]); $downloadFile = $this->crowdin->ai->downloadFileTranslation(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); $this->assertInstanceOf(DownloadFile::class, $downloadFile); - $this->assertEquals('https://example.com/download/file.json', $downloadFile->getUrl()); - $this->assertEquals('2026-01-23T12:00:00+00:00', $downloadFile->getExpireIn()); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); } public function testDownloadFileTranslationStrings(): void @@ -151,15 +175,1065 @@ public function testDownloadFileTranslationStrings(): void 'method' => 'get', 'response' => '{ "data": { - "url": "https://example.com/download/strings.json", - "expireIn": "2026-01-23T12:00:00+00:00" + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" } }', ]); $downloadFile = $this->crowdin->ai->downloadFileTranslationStrings(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); $this->assertInstanceOf(DownloadFile::class, $downloadFile); - $this->assertEquals('https://example.com/download/strings.json', $downloadFile->getUrl()); - $this->assertEquals('2026-01-23T12:00:00+00:00', $downloadFile->getExpireIn()); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); + } + + public function testListPrompts(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": {}, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $prompts = $this->crowdin->ai->listPrompts(1); + $this->assertInstanceOf(ModelCollection::class, $prompts); + $this->assertInstanceOf(AiPrompt::class, $prompts[0]); + $this->assertEquals(2, $prompts[0]->getId()); + $this->assertEquals('Pre-translate prompt', $prompts[0]->getName()); + $this->assertEquals('pre_translate', $prompts[0]->getAction()); + } + + public function testCreatePrompt(): void + { + $params = [ + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'config' => ['mode' => 'basic'], + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": {"mode": "basic"}, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $prompt = $this->crowdin->ai->createPrompt(1, $params); + $this->assertInstanceOf(AiPrompt::class, $prompt); + $this->assertEquals(2, $prompt->getId()); + $this->assertEquals('Pre-translate prompt', $prompt->getName()); + $this->assertTrue($prompt->isEnabled()); + } + + public function testGetPrompt(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": { + "mode": "basic", + "snippets": ["%custom:companyDescription%"], + "otherLanguageTranslations": { + "isEnabled": true, + "languageIds": ["uk"] + }, + "glossaryTerms": true, + "tmSuggestions": true, + "fileContext": true, + "generateFileSummary": true, + "screenshots": true, + "projectContext": true, + "siblingsStrings": true, + "retryOnQaIssues": true + }, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $prompt = $this->crowdin->ai->getPrompt(1, 2); + $this->assertInstanceOf(AiPrompt::class, $prompt); + $this->assertEquals(2, $prompt->getId()); + $this->assertEquals(2, $prompt->getAiProviderId()); + $this->assertEquals('gpt-5.4', $prompt->getAiModelId()); + $this->assertTrue($prompt->isFineTuningAvailable()); + $this->assertEquals(42, $prompt->getUsageCount()); + } + + public function testDeletePrompt(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deletePrompt(1, 2); + } + + public function testClonePrompt(): void + { + $params = [ + 'name' => 'Pre-translate prompt', + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/clones', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": {}, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $cloned = $this->crowdin->ai->clonePrompt(1, 2, $params); + $this->assertInstanceOf(AiPrompt::class, $cloned); + $this->assertEquals(2, $cloned->getId()); + $this->assertEquals('Pre-translate prompt', $cloned->getName()); + } + + public function testCreatePromptCompletion(): void + { + $params = [ + 'resources' => [ + 'projectId' => 123, + 'targetLanguageId' => 'uk', + 'stringIds' => [78253], + ], + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/completions', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "aiPromptId": 38 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $completion = $this->crowdin->ai->createPromptCompletion(1, 2, $params); + $this->assertInstanceOf(AiPromptCompletion::class, $completion); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $completion->getIdentifier()); + $this->assertEquals('finished', $completion->getStatus()); + $this->assertEquals(['aiPromptId' => 38], $completion->getAttributes()); + } + + public function testGetPromptCompletion(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "aiPromptId": 38 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $completion = $this->crowdin->ai->getPromptCompletion(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiPromptCompletion::class, $completion); + $this->assertEquals('finished', $completion->getStatus()); + $this->assertEquals(100, $completion->getProgress()); + } + + public function testDeletePromptCompletion(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deletePromptCompletion(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + } + + public function testDownloadPromptCompletion(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + 'method' => 'get', + 'response' => '{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" + } + }', + ]); + + $download = $this->crowdin->ai->downloadPromptCompletion(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); + } + + public function testCreateFineTuningDataset(): void + { + $params = [ + 'projectIds' => [1, 2, 3], + 'tmIds' => [4, 5], + 'purpose' => 'training', + 'dateFrom' => '2019-09-23T11:26:54+00:00', + 'dateTo' => '2019-09-23T11:26:54+00:00', + 'maxFileSize' => 1000000, + 'minExamplesCount' => 100, + 'maxExamplesCount' => 10000, + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/fine-tuning/datasets', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "projectIds": [1, 2, 3], + "tmIds": [4, 5], + "purpose": "training", + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": 1000000, + "minExamplesCount": 100, + "maxExamplesCount": 10000 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $dataset = $this->crowdin->ai->createFineTuningDataset(1, 2, $params); + $this->assertInstanceOf(AiFineTuningDataset::class, $dataset); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $dataset->getIdentifier()); + $this->assertEquals('finished', $dataset->getStatus()); + } + + public function testGetFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/fine-tuning/datasets/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "projectIds": [1, 2, 3], + "tmIds": [4, 5, 6], + "purpose": "training", + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": 1000000, + "minExamplesCount": 100, + "maxExamplesCount": 10000 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $dataset = $this->crowdin->ai->getFineTuningDataset(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiFineTuningDataset::class, $dataset); + $this->assertEquals('finished', $dataset->getStatus()); + $this->assertEquals(100, $dataset->getProgress()); + } + + public function testDownloadFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/fine-tuning/datasets/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + 'method' => 'get', + 'response' => '{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" + } + }', + ]); + + $download = $this->crowdin->ai->downloadFineTuningDataset(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); + } + + public function testCreateFineTuningJob(): void + { + $params = [ + 'trainingOptions' => [ + 'projectIds' => [123], + ], + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/fine-tuning/jobs', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "dryRun": false, + "aiPromptId": 789, + "baseModel": "gpt-4o-mini-2024-07-18", + "fineTunedModel": "ft:gpt-4o-mini-2024-07-18:2024-08-12:9vQgFOfp", + "trainedTokensCount": 5000, + "trainingDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/training-dataset.jsonl", + "validationDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/validation-dataset.jsonl", + "metadata": { + "cost": 25.50, + "costCurrency": "USD" + } + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $job = $this->crowdin->ai->createFineTuningJob(1, 2, $params); + $this->assertInstanceOf(AiFineTuningJob::class, $job); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $job->getIdentifier()); + $this->assertEquals('finished', $job->getStatus()); + } + + public function testGetFineTuningJob(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/fine-tuning/jobs/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "dryRun": true, + "aiPromptId": 0, + "hyperparameters": { + "batchSize": null, + "learningRateMultiplier": null, + "nEpochs": null + }, + "trainingOptions": { + "projectIds": null, + "tmIds": null, + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": null, + "minExamplesCount": null, + "maxExamplesCount": null + }, + "validationOptions": { + "projectIds": null, + "tmIds": null, + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": null, + "minExamplesCount": null, + "maxExamplesCount": null + }, + "baseModel": "gpt-4o-mini-2024-07-18", + "fineTunedModel": "ft:gpt-4o-mini-2024-07-18:2024-08-12:9vQgFOfp", + "trainedTokensCount": null, + "trainingDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/training-dataset.jsonl", + "validationDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/validation-dataset.jsonl", + "metadata": { + "cost": 0.0, + "costCurrency": "USD" + } + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $job = $this->crowdin->ai->getFineTuningJob(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiFineTuningJob::class, $job); + $this->assertEquals('finished', $job->getStatus()); + $this->assertEquals(100, $job->getProgress()); + } + + public function testListFineTuningJobs(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/fine-tuning/jobs', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "dryRun": false, + "aiPromptId": 0, + "baseModel": "gpt-4o-mini-2024-07-18", + "fineTunedModel": "ft:gpt-4o-mini-2024-07-18:2024-08-12:9vQgFOfp", + "trainedTokensCount": null, + "trainingDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/training-dataset.jsonl", + "validationDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/validation-dataset.jsonl", + "metadata": { + "cost": 0.0, + "costCurrency": "USD" + } + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $jobs = $this->crowdin->ai->listFineTuningJobs(1); + $this->assertInstanceOf(ModelCollection::class, $jobs); + $this->assertInstanceOf(AiFineTuningJob::class, $jobs[0]); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $jobs[0]->getIdentifier()); + } + + public function testListFineTuningJobEvents(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/prompts/2/fine-tuning/jobs/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/events', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": "ftevent-0HOW0hqwQ7G1b70qOC8S7UsT", + "type": "message", + "message": "Created fine-tuning job: ftjob-EhnfVT9MnddMBLfjMcme71if", + "data": null, + "createdAt": "2019-09-23T11:26:54+00:00" + } + }, + { + "data": { + "id": "ftevent-0HOW0hqwQ7G1b70qOC8S7UsT", + "type": "metrics", + "message": null, + "data": { + "step": 135, + "totalSteps": 135, + "trainingLoss": 0.0031, + "validationLoss": 0.0449, + "fullValidationLoss": 0.0275 + }, + "createdAt": "2019-09-23T11:26:54+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $events = $this->crowdin->ai->listFineTuningJobEvents(1, 2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(ModelCollection::class, $events); + $this->assertInstanceOf(AiFineTuningEvent::class, $events[0]); + $this->assertEquals('ftevent-0HOW0hqwQ7G1b70qOC8S7UsT', $events[0]->getId()); + $this->assertEquals('message', $events[0]->getType()); + $this->assertEquals('Created fine-tuning job: ftjob-EhnfVT9MnddMBLfjMcme71if', $events[0]->getMessage()); + } + + public function testListProviders(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/providers', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "name": "OpenAI", + "type": "open_ai", + "credentials": {"apiKey": "sk-..."}, + "config": { + "actionRules": [ + { + "action": "pre_translate", + "availableAiModelIds": ["gpt-5.4"] + } + ] + }, + "isEnabled": true, + "useSystemCredentials": false, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00", + "promptsCount": 42 + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $providers = $this->crowdin->ai->listProviders(1); + $this->assertInstanceOf(ModelCollection::class, $providers); + $this->assertInstanceOf(AiProvider::class, $providers[0]); + $this->assertEquals(2, $providers[0]->getId()); + $this->assertEquals('OpenAI', $providers[0]->getName()); + $this->assertEquals('open_ai', $providers[0]->getType()); + } + + public function testCreateProvider(): void + { + $params = [ + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => ['apiKey' => 'sk-...'], + 'isEnabled' => true, + 'useSystemCredentials' => false, + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/providers', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "name": "OpenAI", + "type": "open_ai", + "credentials": {"apiKey": "sk-..."}, + "config": {}, + "isEnabled": true, + "useSystemCredentials": false, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00", + "promptsCount": 42 + } + }', + ]); + + $provider = $this->crowdin->ai->createProvider(1, $params); + $this->assertInstanceOf(AiProvider::class, $provider); + $this->assertEquals(2, $provider->getId()); + $this->assertEquals('OpenAI', $provider->getName()); + $this->assertEquals(42, $provider->getPromptsCount()); + } + + public function testGetProvider(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/providers/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "name": "OpenAI", + "type": "open_ai", + "credentials": {"apiKey": "string"}, + "config": { + "actionRules": [ + { + "action": "pre_translate", + "availableAiModelIds": ["gpt-5.4"] + } + ] + }, + "isEnabled": true, + "useSystemCredentials": false, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00", + "promptsCount": 42 + } + }', + ]); + + $provider = $this->crowdin->ai->getProvider(1, 2); + $this->assertInstanceOf(AiProvider::class, $provider); + $this->assertEquals(2, $provider->getId()); + $this->assertFalse($provider->isUseSystemCredentials()); + $this->assertEquals(42, $provider->getPromptsCount()); + } + + public function testDeleteProvider(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/providers/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deleteProvider(1, 2); + } + + public function testListProviderModels(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/providers/2/models', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": "gpt-5.4", + "provider": "open_ai", + "providerName": "OpenAI", + "providerId": 1 + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $models = $this->crowdin->ai->listProviderModels(1, 2); + $this->assertInstanceOf(ModelCollection::class, $models); + $this->assertInstanceOf(AiProviderModel::class, $models[0]); + $this->assertEquals('gpt-5.4', $models[0]->getId()); + $this->assertEquals('open_ai', $models[0]->getProvider()); + $this->assertEquals(1, $models[0]->getProviderId()); + } + + public function testCreateProviderChatCompletion(): void + { + $params = [ + 'model' => 'gpt-4o', + 'messages' => [ + ['role' => 'user', 'content' => 'Hello'], + ], + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/providers/2/chat/completions', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": "chatcmpl-123", + "object": "chat.completion", + "model": "gpt-4o", + "choices": [{"message": {"role": "assistant", "content": "Hi!"}}] + } + }', + ]); + + $completion = $this->crowdin->ai->createProviderChatCompletion(1, 2, $params); + $this->assertInstanceOf(AiProxyChatCompletion::class, $completion); + } + + public function testGenerateReport(): void + { + $params = [ + 'type' => 'tokens-usage-raw-data', + 'schema' => [ + 'dateFrom' => '2024-01-23T07:00:14+00:00', + 'dateTo' => '2024-09-27T07:00:14+00:00', + 'format' => 'json', + 'projectIds' => [22], + 'promptIds' => [18], + 'userIds' => [1], + ], + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/reports', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "format": "json", + "reportType": "tokens-usage-raw-data", + "schema": {} + }, + "createdAt": "2024-01-23T11:26:54+00:00", + "updatedAt": "2024-09-23T11:26:54+00:00", + "startedAt": "2024-05-23T11:26:54+00:00", + "finishedAt": "2024-05-23T11:26:54+00:00" + } + }', + ]); + + $report = $this->crowdin->ai->generateReport(1, $params); + $this->assertInstanceOf(AiReport::class, $report); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); + $this->assertEquals('finished', $report->getStatus()); + } + + public function testGetReport(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "format": "json", + "reportType": "tokens-usage-raw-data", + "schema": {} + }, + "createdAt": "2024-01-23T11:26:54+00:00", + "updatedAt": "2024-09-23T11:26:54+00:00", + "startedAt": "2024-05-23T11:26:54+00:00", + "finishedAt": "2024-05-23T11:26:54+00:00" + } + }', + ]); + + $report = $this->crowdin->ai->getReport(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiReport::class, $report); + $this->assertEquals('finished', $report->getStatus()); + $this->assertEquals(100, $report->getProgress()); + } + + public function testDownloadReport(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + 'method' => 'get', + 'response' => '{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" + } + }', + ]); + + $download = $this->crowdin->ai->downloadReport(1, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); + } + + public function testGetSettings(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings', + 'method' => 'get', + 'response' => '{ + "data": { + "preTranslationAiPromptId": 2, + "editorSuggestionAiPromptId": 5, + "qaCheckActionAiPromptId": 8, + "contextReviewAiPromptId": 11 + } + }', + ]); + + $settings = $this->crowdin->ai->getSettings(1); + $this->assertInstanceOf(AiSettings::class, $settings); + $this->assertEquals(2, $settings->getPreTranslationAiPromptId()); + $this->assertEquals(5, $settings->getEditorSuggestionAiPromptId()); + $this->assertEquals(8, $settings->getQaCheckActionAiPromptId()); + $this->assertEquals(11, $settings->getContextReviewAiPromptId()); + } + + public function testListCustomPlaceholders(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings/custom-placeholders', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $placeholders = $this->crowdin->ai->listCustomPlaceholders(1); + $this->assertInstanceOf(ModelCollection::class, $placeholders); + $this->assertInstanceOf(AiCustomPlaceholder::class, $placeholders[0]); + $this->assertEquals(2, $placeholders[0]->getId()); + $this->assertEquals('%custom:productDescription%', $placeholders[0]->getPlaceholder()); + } + + public function testCreateCustomPlaceholder(): void + { + $params = [ + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/settings/custom-placeholders', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $placeholder = $this->crowdin->ai->createCustomPlaceholder(1, $params); + $this->assertInstanceOf(AiCustomPlaceholder::class, $placeholder); + $this->assertEquals(2, $placeholder->getId()); + $this->assertEquals('Product description', $placeholder->getDescription()); + } + + public function testGetCustomPlaceholder(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings/custom-placeholders/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $placeholder = $this->crowdin->ai->getCustomPlaceholder(1, 2); + $this->assertInstanceOf(AiCustomPlaceholder::class, $placeholder); + $this->assertEquals( + 'The product is the professional consulting service that transform challenges into opportunities.', + $placeholder->getValue() + ); + } + + public function testDeleteCustomPlaceholder(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings/custom-placeholders/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deleteCustomPlaceholder(1, 2); + } + + public function testListSnippets(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings/snippets', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $snippets = $this->crowdin->ai->listSnippets(1); + $this->assertInstanceOf(ModelCollection::class, $snippets); + $this->assertInstanceOf(AiSnippet::class, $snippets[0]); + $this->assertEquals(2, $snippets[0]->getId()); + $this->assertEquals('%custom:productDescription%', $snippets[0]->getPlaceholder()); + } + + public function testCreateSnippet(): void + { + $params = [ + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + ]; + + $this->mockRequest([ + 'path' => '/users/1/ai/settings/snippets', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $snippet = $this->crowdin->ai->createSnippet(1, $params); + $this->assertInstanceOf(AiSnippet::class, $snippet); + $this->assertEquals(2, $snippet->getId()); + $this->assertEquals('Product description', $snippet->getDescription()); + } + + public function testGetSnippet(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings/snippets/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $snippet = $this->crowdin->ai->getSnippet(1, 2); + $this->assertInstanceOf(AiSnippet::class, $snippet); + $this->assertEquals( + 'The product is the professional consulting service that transform challenges into opportunities.', + $snippet->getValue() + ); + } + + public function testDeleteSnippet(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/settings/snippets/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deleteSnippet(1, 2); } } diff --git a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php index 526e8fd3..b43e941d 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php @@ -2,9 +2,22 @@ namespace CrowdinApiClient\Tests\Api\Enterprise; +use CrowdinApiClient\Model\AiCustomPlaceholder; use CrowdinApiClient\Model\AiFileTranslation; +use CrowdinApiClient\Model\AiFineTuningDataset; +use CrowdinApiClient\Model\AiFineTuningEvent; +use CrowdinApiClient\Model\AiFineTuningJob; +use CrowdinApiClient\Model\AiPrompt; +use CrowdinApiClient\Model\AiPromptCompletion; +use CrowdinApiClient\Model\AiProvider; +use CrowdinApiClient\Model\AiProviderModel; +use CrowdinApiClient\Model\AiProxyChatCompletion; +use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiSettings; +use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; use CrowdinApiClient\Model\DownloadFile; +use CrowdinApiClient\ModelCollection; class AiApiTest extends AbstractTestApi { @@ -12,6 +25,7 @@ public function testTranslateStrings(): void { $params = [ 'strings' => ['Some text to translate!'], + 'sourceLanguageId' => 'en', 'targetLanguageId' => 'uk', ]; @@ -24,7 +38,8 @@ public function testTranslateStrings(): void "sourceLanguageId": "en", "targetLanguageId": "uk", "translations": [ - "Перекладений текст 1" + "Перекладений текст 1", + "Перекладений текст 2" ] } }', @@ -34,14 +49,23 @@ public function testTranslateStrings(): void $this->assertInstanceOf(AiTranslation::class, $aiTranslation); $this->assertEquals('en', $aiTranslation->getSourceLanguageId()); $this->assertEquals('uk', $aiTranslation->getTargetLanguageId()); - $this->assertEquals(['Перекладений текст 1'], $aiTranslation->getTranslations()); + $this->assertEquals(['Перекладений текст 1', 'Перекладений текст 2'], $aiTranslation->getTranslations()); } public function testCreateFileTranslation(): void { $params = [ 'storageId' => 123, + 'sourceLanguageId' => 'en', 'targetLanguageId' => 'uk', + 'type' => 'xliff', + 'parserVersion' => 1, + 'tmIds' => [123], + 'glossaryIds' => [456], + 'styleGuideIds' => [654], + 'aiPromptId' => 789, + 'instructions' => ['Keep a formal tone'], + 'attachmentIds' => [123], ]; $this->mockRequest([ @@ -54,9 +78,9 @@ public function testCreateFileTranslation(): void "status": "finished", "progress": 100, "attributes": { - "stage": "done", + "stage": "translate", "error": null, - "downloadName": "Sample_Chrome.json", + "downloadName": "file.pdf", "sourceLanguageId": "en", "targetLanguageId": "uk", "originalFileName": "Sample_Chrome.json", @@ -87,22 +111,22 @@ public function testGetFileTranslation(): void 'response' => '{ "data": { "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", - "status": "in_progress", - "progress": 50, + "status": "finished", + "progress": 100, "attributes": { "stage": "translate", "error": null, - "downloadName": null, + "downloadName": "file.pdf", "sourceLanguageId": "en", "targetLanguageId": "uk", "originalFileName": "Sample_Chrome.json", - "detectedType": null, - "parserVersion": null + "detectedType": "chrome", + "parserVersion": 2 }, "createdAt": "2026-01-23T11:26:54+00:00", "updatedAt": "2026-01-23T11:26:54+00:00", "startedAt": "2026-01-23T11:26:54+00:00", - "finishedAt": null + "finishedAt": "2026-01-23T11:26:54+00:00" } }', ]); @@ -110,8 +134,8 @@ public function testGetFileTranslation(): void $fileTranslation = $this->crowdin->ai->getFileTranslation('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); $this->assertInstanceOf(AiFileTranslation::class, $fileTranslation); $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); - $this->assertEquals('in_progress', $fileTranslation->getStatus()); - $this->assertEquals(50, $fileTranslation->getProgress()); + $this->assertEquals('finished', $fileTranslation->getStatus()); + $this->assertEquals(100, $fileTranslation->getProgress()); } public function testDeleteFileTranslation(): void @@ -132,16 +156,16 @@ public function testDownloadFileTranslation(): void 'method' => 'get', 'response' => '{ "data": { - "url": "https://example.com/download/file.json", - "expireIn": "2026-01-23T12:00:00+00:00" + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" } }', ]); $downloadFile = $this->crowdin->ai->downloadFileTranslation('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); $this->assertInstanceOf(DownloadFile::class, $downloadFile); - $this->assertEquals('https://example.com/download/file.json', $downloadFile->getUrl()); - $this->assertEquals('2026-01-23T12:00:00+00:00', $downloadFile->getExpireIn()); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); } public function testDownloadFileTranslationStrings(): void @@ -151,15 +175,1020 @@ public function testDownloadFileTranslationStrings(): void 'method' => 'get', 'response' => '{ "data": { - "url": "https://example.com/download/strings.json", - "expireIn": "2026-01-23T12:00:00+00:00" + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" } }', ]); $downloadFile = $this->crowdin->ai->downloadFileTranslationStrings('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); $this->assertInstanceOf(DownloadFile::class, $downloadFile); - $this->assertEquals('https://example.com/download/strings.json', $downloadFile->getUrl()); - $this->assertEquals('2026-01-23T12:00:00+00:00', $downloadFile->getExpireIn()); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $downloadFile->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $downloadFile->getExpireIn()); + } + + public function testListPrompts(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": {}, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $prompts = $this->crowdin->ai->listPrompts(); + $this->assertInstanceOf(ModelCollection::class, $prompts); + $this->assertInstanceOf(AiPrompt::class, $prompts[0]); + $this->assertEquals(2, $prompts[0]->getId()); + $this->assertEquals('pre_translate', $prompts[0]->getAction()); + } + + public function testCreatePrompt(): void + { + $params = [ + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'config' => ['mode' => 'basic'], + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": {"mode": "basic"}, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $prompt = $this->crowdin->ai->createPrompt($params); + $this->assertInstanceOf(AiPrompt::class, $prompt); + $this->assertEquals(2, $prompt->getId()); + $this->assertEquals('Pre-translate prompt', $prompt->getName()); + } + + public function testGetPrompt(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": { + "mode": "basic", + "snippets": ["%custom:companyDescription%"], + "glossaryTerms": true, + "tmSuggestions": true, + "fileContext": true + }, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $prompt = $this->crowdin->ai->getPrompt(2); + $this->assertInstanceOf(AiPrompt::class, $prompt); + $this->assertEquals(2, $prompt->getId()); + $this->assertTrue($prompt->isFineTuningAvailable()); + $this->assertEquals(42, $prompt->getUsageCount()); + } + + public function testDeletePrompt(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deletePrompt(2); + } + + public function testClonePrompt(): void + { + $params = [ + 'name' => 'Pre-translate prompt', + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts/2/clones', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "name": "Pre-translate prompt", + "action": "pre_translate", + "aiProviderId": 2, + "aiModelId": "gpt-5.4", + "isEnabled": true, + "enabledProjectIds": [1], + "config": {}, + "promptPreview": null, + "isFineTuningAvailable": true, + "createdBy": 123, + "updatedBy": 456, + "lastUsedBy": 789, + "lastUsedAt": "2019-09-25T14:30:00+00:00", + "usageCount": 42, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $cloned = $this->crowdin->ai->clonePrompt(2, $params); + $this->assertInstanceOf(AiPrompt::class, $cloned); + $this->assertEquals(2, $cloned->getId()); + $this->assertEquals('Pre-translate prompt', $cloned->getName()); + } + + public function testCreatePromptCompletion(): void + { + $params = [ + 'resources' => [ + 'projectId' => 123, + 'targetLanguageId' => 'uk', + 'stringIds' => [78253], + ], + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts/2/completions', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "aiPromptId": 38 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $completion = $this->crowdin->ai->createPromptCompletion(2, $params); + $this->assertInstanceOf(AiPromptCompletion::class, $completion); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $completion->getIdentifier()); + $this->assertEquals('finished', $completion->getStatus()); + } + + public function testGetPromptCompletion(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "aiPromptId": 38 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $completion = $this->crowdin->ai->getPromptCompletion(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiPromptCompletion::class, $completion); + $this->assertEquals('finished', $completion->getStatus()); + $this->assertEquals(100, $completion->getProgress()); + } + + public function testDeletePromptCompletion(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deletePromptCompletion(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + } + + public function testDownloadPromptCompletion(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/completions/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + 'method' => 'get', + 'response' => '{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" + } + }', + ]); + + $download = $this->crowdin->ai->downloadPromptCompletion(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); + } + + public function testCreateFineTuningDataset(): void + { + $params = [ + 'projectIds' => [1, 2, 3], + 'tmIds' => [4, 5], + 'purpose' => 'training', + 'dateFrom' => '2019-09-23T11:26:54+00:00', + 'dateTo' => '2019-09-23T11:26:54+00:00', + 'maxFileSize' => 1000000, + 'minExamplesCount' => 100, + 'maxExamplesCount' => 10000, + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts/2/fine-tuning/datasets', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "projectIds": [1, 2, 3], + "tmIds": [4, 5], + "purpose": "training", + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": 1000000, + "minExamplesCount": 100, + "maxExamplesCount": 10000 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $dataset = $this->crowdin->ai->createFineTuningDataset(2, $params); + $this->assertInstanceOf(AiFineTuningDataset::class, $dataset); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $dataset->getIdentifier()); + $this->assertEquals('finished', $dataset->getStatus()); + } + + public function testGetFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/fine-tuning/datasets/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "projectIds": [1, 2, 3], + "tmIds": [4, 5, 6], + "purpose": "training", + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": 1000000, + "minExamplesCount": 100, + "maxExamplesCount": 10000 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $dataset = $this->crowdin->ai->getFineTuningDataset(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiFineTuningDataset::class, $dataset); + $this->assertEquals('finished', $dataset->getStatus()); + $this->assertEquals(100, $dataset->getProgress()); + } + + public function testDownloadFineTuningDataset(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/fine-tuning/datasets/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + 'method' => 'get', + 'response' => '{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" + } + }', + ]); + + $download = $this->crowdin->ai->downloadFineTuningDataset(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); + } + + public function testCreateFineTuningJob(): void + { + $params = [ + 'trainingOptions' => [ + 'projectIds' => [123], + ], + ]; + + $this->mockRequest([ + 'path' => '/ai/prompts/2/fine-tuning/jobs', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "dryRun": false, + "aiPromptId": 789, + "baseModel": "gpt-4o-mini-2024-07-18", + "fineTunedModel": "ft:gpt-4o-mini-2024-07-18:2024-08-12:9vQgFOfp", + "trainedTokensCount": 5000, + "trainingDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/training-dataset.jsonl", + "validationDatasetUrl": "https://crowdin-tmp.s3.eu-central-1.amazonaws.com/72057531/ai_fine_tuning/0fc00e9e-3a80-49d1-a8cb-39c9a81c0ae2/validation-dataset.jsonl", + "metadata": { + "cost": 25.50, + "costCurrency": "USD" + } + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $job = $this->crowdin->ai->createFineTuningJob(2, $params); + $this->assertInstanceOf(AiFineTuningJob::class, $job); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $job->getIdentifier()); + $this->assertEquals('finished', $job->getStatus()); + } + + public function testGetFineTuningJob(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/fine-tuning/jobs/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "dryRun": true, + "aiPromptId": 0, + "baseModel": "gpt-4o-mini-2024-07-18", + "fineTunedModel": "ft:gpt-4o-mini-2024-07-18:2024-08-12:9vQgFOfp", + "trainedTokensCount": null, + "metadata": { + "cost": 0.0, + "costCurrency": "USD" + } + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + }', + ]); + + $job = $this->crowdin->ai->getFineTuningJob(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiFineTuningJob::class, $job); + $this->assertEquals('finished', $job->getStatus()); + $this->assertEquals(100, $job->getProgress()); + } + + public function testListFineTuningJobs(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/fine-tuning/jobs', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "dryRun": false, + "aiPromptId": 0, + "baseModel": "gpt-4o-mini-2024-07-18", + "fineTunedModel": "ft:gpt-4o-mini-2024-07-18:2024-08-12:9vQgFOfp", + "trainedTokensCount": null, + "metadata": { + "cost": 0.0, + "costCurrency": "USD" + } + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $jobs = $this->crowdin->ai->listFineTuningJobs(); + $this->assertInstanceOf(ModelCollection::class, $jobs); + $this->assertInstanceOf(AiFineTuningJob::class, $jobs[0]); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $jobs[0]->getIdentifier()); + } + + public function testListFineTuningJobEvents(): void + { + $this->mockRequest([ + 'path' => '/ai/prompts/2/fine-tuning/jobs/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/events', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": "ftevent-0HOW0hqwQ7G1b70qOC8S7UsT", + "type": "message", + "message": "Created fine-tuning job: ftjob-EhnfVT9MnddMBLfjMcme71if", + "data": null, + "createdAt": "2019-09-23T11:26:54+00:00" + } + }, + { + "data": { + "id": "ftevent-0HOW0hqwQ7G1b70qOC8S7UsT", + "type": "metrics", + "message": null, + "data": { + "step": 135, + "totalSteps": 135, + "trainingLoss": 0.0031, + "validationLoss": 0.0449, + "fullValidationLoss": 0.0275 + }, + "createdAt": "2019-09-23T11:26:54+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $events = $this->crowdin->ai->listFineTuningJobEvents(2, '50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(ModelCollection::class, $events); + $this->assertInstanceOf(AiFineTuningEvent::class, $events[0]); + $this->assertEquals('ftevent-0HOW0hqwQ7G1b70qOC8S7UsT', $events[0]->getId()); + $this->assertEquals('Created fine-tuning job: ftjob-EhnfVT9MnddMBLfjMcme71if', $events[0]->getMessage()); + } + + public function testListProviders(): void + { + $this->mockRequest([ + 'path' => '/ai/providers', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "name": "OpenAI", + "type": "open_ai", + "credentials": {"apiKey": "sk-..."}, + "config": { + "actionRules": [ + { + "action": "pre_translate", + "availableAiModelIds": ["gpt-5.4"] + } + ] + }, + "isEnabled": true, + "useSystemCredentials": false, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00", + "promptsCount": 42 + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $providers = $this->crowdin->ai->listProviders(); + $this->assertInstanceOf(ModelCollection::class, $providers); + $this->assertInstanceOf(AiProvider::class, $providers[0]); + $this->assertEquals(2, $providers[0]->getId()); + $this->assertEquals('OpenAI', $providers[0]->getName()); + } + + public function testCreateProvider(): void + { + $params = [ + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => ['apiKey' => 'sk-...'], + 'isEnabled' => true, + 'useSystemCredentials' => false, + ]; + + $this->mockRequest([ + 'path' => '/ai/providers', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "name": "OpenAI", + "type": "open_ai", + "credentials": {"apiKey": "sk-..."}, + "config": {}, + "isEnabled": true, + "useSystemCredentials": false, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00", + "promptsCount": 42 + } + }', + ]); + + $provider = $this->crowdin->ai->createProvider($params); + $this->assertInstanceOf(AiProvider::class, $provider); + $this->assertEquals(2, $provider->getId()); + $this->assertEquals('OpenAI', $provider->getName()); + } + + public function testGetProvider(): void + { + $this->mockRequest([ + 'path' => '/ai/providers/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "name": "OpenAI", + "type": "open_ai", + "credentials": {"apiKey": "string"}, + "config": { + "actionRules": [ + { + "action": "pre_translate", + "availableAiModelIds": ["gpt-5.4"] + } + ] + }, + "isEnabled": true, + "useSystemCredentials": false, + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00", + "promptsCount": 42 + } + }', + ]); + + $provider = $this->crowdin->ai->getProvider(2); + $this->assertInstanceOf(AiProvider::class, $provider); + $this->assertEquals(2, $provider->getId()); + $this->assertFalse($provider->isUseSystemCredentials()); + $this->assertEquals(42, $provider->getPromptsCount()); + } + + public function testDeleteProvider(): void + { + $this->mockRequest([ + 'path' => '/ai/providers/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deleteProvider(2); + } + + public function testListProviderModels(): void + { + $this->mockRequest([ + 'path' => '/ai/providers/2/models', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": "gpt-5.4", + "provider": "open_ai", + "providerName": "OpenAI", + "providerId": 1 + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $models = $this->crowdin->ai->listProviderModels(2); + $this->assertInstanceOf(ModelCollection::class, $models); + $this->assertInstanceOf(AiProviderModel::class, $models[0]); + $this->assertEquals('gpt-5.4', $models[0]->getId()); + $this->assertEquals('open_ai', $models[0]->getProvider()); + } + + public function testCreateProviderChatCompletion(): void + { + $params = [ + 'model' => 'gpt-4o', + 'messages' => [ + ['role' => 'user', 'content' => 'Hello'], + ], + ]; + + $this->mockRequest([ + 'path' => '/ai/providers/2/chat/completions', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": "chatcmpl-123", + "object": "chat.completion", + "model": "gpt-4o", + "choices": [{"message": {"role": "assistant", "content": "Hi!"}}] + } + }', + ]); + + $completion = $this->crowdin->ai->createProviderChatCompletion(2, $params); + $this->assertInstanceOf(AiProxyChatCompletion::class, $completion); + } + + public function testGenerateReport(): void + { + $params = [ + 'type' => 'tokens-usage-raw-data', + 'schema' => [ + 'dateFrom' => '2024-01-23T07:00:14+00:00', + 'dateTo' => '2024-09-27T07:00:14+00:00', + 'format' => 'json', + 'projectIds' => [22], + 'promptIds' => [18], + 'userIds' => [1], + ], + ]; + + $this->mockRequest([ + 'path' => '/ai/reports', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "format": "json", + "reportType": "tokens-usage-raw-data", + "schema": {} + }, + "createdAt": "2024-01-23T11:26:54+00:00", + "updatedAt": "2024-09-23T11:26:54+00:00", + "startedAt": "2024-05-23T11:26:54+00:00", + "finishedAt": "2024-05-23T11:26:54+00:00" + } + }', + ]); + + $report = $this->crowdin->ai->generateReport($params); + $this->assertInstanceOf(AiReport::class, $report); + $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $report->getIdentifier()); + $this->assertEquals('finished', $report->getStatus()); + } + + public function testGetReport(): void + { + $this->mockRequest([ + 'path' => '/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3', + 'method' => 'get', + 'response' => '{ + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "format": "json", + "reportType": "tokens-usage-raw-data", + "schema": {} + }, + "createdAt": "2024-01-23T11:26:54+00:00", + "updatedAt": "2024-09-23T11:26:54+00:00", + "startedAt": "2024-05-23T11:26:54+00:00", + "finishedAt": "2024-05-23T11:26:54+00:00" + } + }', + ]); + + $report = $this->crowdin->ai->getReport('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(AiReport::class, $report); + $this->assertEquals('finished', $report->getStatus()); + $this->assertEquals(100, $report->getProgress()); + } + + public function testDownloadReport(): void + { + $this->mockRequest([ + 'path' => '/ai/reports/50fb3506-4127-4ba8-8296-f97dc7e3e0c3/download', + 'method' => 'get', + 'response' => '{ + "data": { + "url": "https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff", + "expireIn": "2019-09-20T10:31:21+00:00" + } + }', + ]); + + $download = $this->crowdin->ai->downloadReport('50fb3506-4127-4ba8-8296-f97dc7e3e0c3'); + $this->assertInstanceOf(DownloadFile::class, $download); + $this->assertEquals('https://production-enterprise-importer.downloads.crowdin.com/992000002/2/14.xliff', $download->getUrl()); + $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); + } + + public function testGetSettings(): void + { + $this->mockRequest([ + 'path' => '/ai/settings', + 'method' => 'get', + 'response' => '{ + "data": { + "preTranslationAiPromptId": 2, + "editorSuggestionAiPromptId": 5, + "qaCheckActionAiPromptId": 8, + "contextReviewAiPromptId": 11 + } + }', + ]); + + $settings = $this->crowdin->ai->getSettings(); + $this->assertInstanceOf(AiSettings::class, $settings); + $this->assertEquals(2, $settings->getPreTranslationAiPromptId()); + $this->assertEquals(5, $settings->getEditorSuggestionAiPromptId()); + $this->assertEquals(8, $settings->getQaCheckActionAiPromptId()); + $this->assertEquals(11, $settings->getContextReviewAiPromptId()); + } + + public function testListCustomPlaceholders(): void + { + $this->mockRequest([ + 'path' => '/ai/settings/custom-placeholders', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $placeholders = $this->crowdin->ai->listCustomPlaceholders(); + $this->assertInstanceOf(ModelCollection::class, $placeholders); + $this->assertInstanceOf(AiCustomPlaceholder::class, $placeholders[0]); + $this->assertEquals(2, $placeholders[0]->getId()); + $this->assertEquals('%custom:productDescription%', $placeholders[0]->getPlaceholder()); + } + + public function testCreateCustomPlaceholder(): void + { + $params = [ + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + ]; + + $this->mockRequest([ + 'path' => '/ai/settings/custom-placeholders', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $placeholder = $this->crowdin->ai->createCustomPlaceholder($params); + $this->assertInstanceOf(AiCustomPlaceholder::class, $placeholder); + $this->assertEquals(2, $placeholder->getId()); + $this->assertEquals('Product description', $placeholder->getDescription()); + } + + public function testGetCustomPlaceholder(): void + { + $this->mockRequest([ + 'path' => '/ai/settings/custom-placeholders/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $placeholder = $this->crowdin->ai->getCustomPlaceholder(2); + $this->assertInstanceOf(AiCustomPlaceholder::class, $placeholder); + $this->assertEquals( + 'The product is the professional consulting service that transform challenges into opportunities.', + $placeholder->getValue() + ); + } + + public function testDeleteCustomPlaceholder(): void + { + $this->mockRequest([ + 'path' => '/ai/settings/custom-placeholders/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deleteCustomPlaceholder(2); + } + + public function testListSnippets(): void + { + $this->mockRequest([ + 'path' => '/ai/settings/snippets', + 'method' => 'get', + 'response' => '{ + "data": [ + { + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + } + ], + "pagination": {"offset": 0, "limit": 25} + }', + ]); + + $snippets = $this->crowdin->ai->listSnippets(); + $this->assertInstanceOf(ModelCollection::class, $snippets); + $this->assertInstanceOf(AiSnippet::class, $snippets[0]); + $this->assertEquals(2, $snippets[0]->getId()); + $this->assertEquals('%custom:productDescription%', $snippets[0]->getPlaceholder()); + } + + public function testCreateSnippet(): void + { + $params = [ + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + ]; + + $this->mockRequest([ + 'path' => '/ai/settings/snippets', + 'method' => 'post', + 'body' => json_encode($params), + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $snippet = $this->crowdin->ai->createSnippet($params); + $this->assertInstanceOf(AiSnippet::class, $snippet); + $this->assertEquals(2, $snippet->getId()); + $this->assertEquals('Product description', $snippet->getDescription()); + } + + public function testGetSnippet(): void + { + $this->mockRequest([ + 'path' => '/ai/settings/snippets/2', + 'method' => 'get', + 'response' => '{ + "data": { + "id": 2, + "description": "Product description", + "placeholder": "%custom:productDescription%", + "value": "The product is the professional consulting service that transform challenges into opportunities.", + "createdAt": "2019-09-20T11:11:05+00:00", + "updatedAt": "2019-09-20T12:22:20+00:00" + } + }', + ]); + + $snippet = $this->crowdin->ai->getSnippet(2); + $this->assertInstanceOf(AiSnippet::class, $snippet); + $this->assertEquals( + 'The product is the professional consulting service that transform challenges into opportunities.', + $snippet->getValue() + ); + } + + public function testDeleteSnippet(): void + { + $this->mockRequest([ + 'path' => '/ai/settings/snippets/2', + 'method' => 'delete', + 'response' => '', + ]); + + $this->crowdin->ai->deleteSnippet(2); } } From 55e6d2b09d2644b2f0874b57ae812d56308ba573 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Sat, 6 Jun 2026 12:24:12 +0300 Subject: [PATCH 2/4] fix: "data" property hiding --- src/CrowdinApiClient/Model/AiFineTuningEvent.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CrowdinApiClient/Model/AiFineTuningEvent.php b/src/CrowdinApiClient/Model/AiFineTuningEvent.php index abd112af..42807161 100644 --- a/src/CrowdinApiClient/Model/AiFineTuningEvent.php +++ b/src/CrowdinApiClient/Model/AiFineTuningEvent.php @@ -16,7 +16,7 @@ class AiFineTuningEvent extends BaseModel protected $message; /** @var array|null */ - protected $data; + protected $metrics; /** @var string */ protected $createdAt; @@ -28,7 +28,7 @@ public function __construct(array $data = []) $this->id = (string)$this->getDataProperty('id'); $this->type = (string)$this->getDataProperty('type'); $this->message = (string)$this->getDataProperty('message'); - $this->data = $this->getDataProperty('data'); + $this->metrics = $this->getDataProperty('data'); $this->createdAt = (string)$this->getDataProperty('createdAt'); } @@ -49,7 +49,7 @@ public function getMessage(): string public function getMetrics(): ?array { - return $this->data; + return $this->metrics; } public function getCreatedAt(): string From ed8d3c20420b9ea0360c0b0ad7fe8510585746e7 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Sat, 6 Jun 2026 12:24:23 +0300 Subject: [PATCH 3/4] test: add more cases --- tests/CrowdinApiClient/Api/AiApiTest.php | 251 ++++++++++++++++++ .../Api/Enterprise/AiApiTest.php | 251 ++++++++++++++++++ 2 files changed, 502 insertions(+) diff --git a/tests/CrowdinApiClient/Api/AiApiTest.php b/tests/CrowdinApiClient/Api/AiApiTest.php index fee3cc1e..eb77ebd8 100644 --- a/tests/CrowdinApiClient/Api/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/AiApiTest.php @@ -136,6 +136,10 @@ public function testGetFileTranslation(): void $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); $this->assertEquals('finished', $fileTranslation->getStatus()); $this->assertEquals(100, $fileTranslation->getProgress()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getCreatedAt()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getUpdatedAt()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getStartedAt()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getFinishedAt()); } public function testDeleteFileTranslation(): void @@ -320,6 +324,15 @@ public function testGetPrompt(): void $this->assertEquals('gpt-5.4', $prompt->getAiModelId()); $this->assertTrue($prompt->isFineTuningAvailable()); $this->assertEquals(42, $prompt->getUsageCount()); + $this->assertEquals([1], $prompt->getEnabledProjectIds()); + $this->assertNull($prompt->getPromptPreview()); + $this->assertEquals(123, $prompt->getCreatedBy()); + $this->assertEquals(456, $prompt->getUpdatedBy()); + $this->assertEquals(789, $prompt->getLastUsedBy()); + $this->assertEquals('2019-09-25T14:30:00+00:00', $prompt->getLastUsedAt()); + $this->assertEquals('2019-09-20T11:11:05+00:00', $prompt->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $prompt->getUpdatedAt()); + $this->assertIsArray($prompt->getConfig()); } public function testDeletePrompt(): void @@ -434,6 +447,10 @@ public function testGetPromptCompletion(): void $this->assertInstanceOf(AiPromptCompletion::class, $completion); $this->assertEquals('finished', $completion->getStatus()); $this->assertEquals(100, $completion->getProgress()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getCreatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getUpdatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getStartedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getFinishedAt()); } public function testDeletePromptCompletion(): void @@ -544,6 +561,11 @@ public function testGetFineTuningDataset(): void $this->assertInstanceOf(AiFineTuningDataset::class, $dataset); $this->assertEquals('finished', $dataset->getStatus()); $this->assertEquals(100, $dataset->getProgress()); + $this->assertIsArray($dataset->getAttributes()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getCreatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getUpdatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getStartedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getFinishedAt()); } public function testDownloadFineTuningDataset(): void @@ -667,6 +689,11 @@ public function testGetFineTuningJob(): void $this->assertInstanceOf(AiFineTuningJob::class, $job); $this->assertEquals('finished', $job->getStatus()); $this->assertEquals(100, $job->getProgress()); + $this->assertIsArray($job->getAttributes()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getCreatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getUpdatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getStartedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getFinishedAt()); } public function testListFineTuningJobs(): void @@ -753,6 +780,8 @@ public function testListFineTuningJobEvents(): void $this->assertEquals('ftevent-0HOW0hqwQ7G1b70qOC8S7UsT', $events[0]->getId()); $this->assertEquals('message', $events[0]->getType()); $this->assertEquals('Created fine-tuning job: ftjob-EhnfVT9MnddMBLfjMcme71if', $events[0]->getMessage()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $events[0]->getCreatedAt()); + $this->assertIsArray($events[1]->getMetrics()); } public function testListProviders(): void @@ -866,6 +895,11 @@ public function testGetProvider(): void $this->assertEquals(2, $provider->getId()); $this->assertFalse($provider->isUseSystemCredentials()); $this->assertEquals(42, $provider->getPromptsCount()); + $this->assertEquals(['apiKey' => 'string'], $provider->getCredentials()); + $this->assertIsArray($provider->getConfig()); + $this->assertTrue($provider->isEnabled()); + $this->assertEquals('2019-09-20T11:11:05+00:00', $provider->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $provider->getUpdatedAt()); } public function testDeleteProvider(): void @@ -1003,6 +1037,11 @@ public function testGetReport(): void $this->assertInstanceOf(AiReport::class, $report); $this->assertEquals('finished', $report->getStatus()); $this->assertEquals(100, $report->getProgress()); + $this->assertIsArray($report->getAttributes()); + $this->assertEquals('2024-01-23T11:26:54+00:00', $report->getCreatedAt()); + $this->assertEquals('2024-09-23T11:26:54+00:00', $report->getUpdatedAt()); + $this->assertEquals('2024-05-23T11:26:54+00:00', $report->getStartedAt()); + $this->assertEquals('2024-05-23T11:26:54+00:00', $report->getFinishedAt()); } public function testDownloadReport(): void @@ -1129,6 +1168,8 @@ public function testGetCustomPlaceholder(): void 'The product is the professional consulting service that transform challenges into opportunities.', $placeholder->getValue() ); + $this->assertEquals('2019-09-20T11:11:05+00:00', $placeholder->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $placeholder->getUpdatedAt()); } public function testDeleteCustomPlaceholder(): void @@ -1224,6 +1265,8 @@ public function testGetSnippet(): void 'The product is the professional consulting service that transform challenges into opportunities.', $snippet->getValue() ); + $this->assertEquals('2019-09-20T11:11:05+00:00', $snippet->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $snippet->getUpdatedAt()); } public function testDeleteSnippet(): void @@ -1236,4 +1279,212 @@ public function testDeleteSnippet(): void $this->crowdin->ai->deleteSnippet(1, 2); } + + public function testUpdatePrompt(): void + { + $prompt = new AiPrompt([ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ]); + $prompt->setName('Updated prompt'); + $prompt->setAction('translate'); + $prompt->setAiProviderId(3); + $prompt->setAiModelId('gpt-4o'); + $prompt->setEnabledProjectIds([2, 3]); + $prompt->setConfig(['mode' => 'advanced']); + + $this->mockRequestPatch( + '/users/1/ai/prompts/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Updated prompt', + 'action' => 'translate', + 'aiProviderId' => 3, + 'aiModelId' => 'gpt-4o', + 'isEnabled' => true, + 'enabledProjectIds' => [2, 3], + 'config' => ['mode' => 'advanced'], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]) + ); + + $updated = $this->crowdin->ai->updatePrompt(1, $prompt); + $this->assertInstanceOf(AiPrompt::class, $updated); + $this->assertEquals(2, $updated->getId()); + $this->assertEquals('Updated prompt', $updated->getName()); + $this->assertEquals('gpt-4o', $updated->getAiModelId()); + } + + public function testUpdateProvider(): void + { + $provider = new AiProvider([ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => ['apiKey' => 'sk-...'], + 'config' => [], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ]); + $provider->setName('Azure OpenAI'); + $provider->setType('azure_open_ai'); + $provider->setCredentials(['apiKey' => 'new-key']); + $provider->setConfig(['actionRules' => []]); + $provider->setIsEnabled(false); + $provider->setUseSystemCredentials(true); + + $this->mockRequestPatch( + '/users/1/ai/providers/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Azure OpenAI', + 'type' => 'azure_open_ai', + 'credentials' => ['apiKey' => 'new-key'], + 'config' => ['actionRules' => []], + 'isEnabled' => false, + 'useSystemCredentials' => true, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ]) + ); + + $updated = $this->crowdin->ai->updateProvider(1, $provider); + $this->assertInstanceOf(AiProvider::class, $updated); + $this->assertEquals(2, $updated->getId()); + $this->assertEquals('Azure OpenAI', $updated->getName()); + } + + public function testUpdateSettings(): void + { + $settings = new AiSettings([ + 'preTranslationAiPromptId' => 2, + 'editorSuggestionAiPromptId' => 5, + 'qaCheckActionAiPromptId' => 8, + 'contextReviewAiPromptId' => 11, + ]); + $settings->setPreTranslationAiPromptId(3); + $settings->setEditorSuggestionAiPromptId(6); + $settings->setQaCheckActionAiPromptId(9); + $settings->setContextReviewAiPromptId(12); + + $this->mockRequestPatch( + '/users/1/ai/settings', + json_encode([ + 'data' => [ + 'preTranslationAiPromptId' => 3, + 'editorSuggestionAiPromptId' => 6, + 'qaCheckActionAiPromptId' => 9, + 'contextReviewAiPromptId' => 12, + ], + ]) + ); + + $updated = $this->crowdin->ai->updateSettings(1, $settings); + $this->assertInstanceOf(AiSettings::class, $updated); + $this->assertEquals(3, $updated->getPreTranslationAiPromptId()); + $this->assertEquals(6, $updated->getEditorSuggestionAiPromptId()); + $this->assertEquals(9, $updated->getQaCheckActionAiPromptId()); + $this->assertEquals(12, $updated->getContextReviewAiPromptId()); + } + + public function testUpdateCustomPlaceholder(): void + { + $placeholder = new AiCustomPlaceholder([ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ]); + $placeholder->setDescription('Updated description'); + $placeholder->setPlaceholder('%custom:updatedDescription%'); + $placeholder->setValue('Updated value.'); + + $this->mockRequestPatch( + '/users/1/ai/settings/custom-placeholders/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'description' => 'Updated description', + 'placeholder' => '%custom:updatedDescription%', + 'value' => 'Updated value.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]) + ); + + $updated = $this->crowdin->ai->updateCustomPlaceholder(1, $placeholder); + $this->assertInstanceOf(AiCustomPlaceholder::class, $updated); + $this->assertEquals('Updated description', $updated->getDescription()); + $this->assertEquals('%custom:updatedDescription%', $updated->getPlaceholder()); + $this->assertEquals('Updated value.', $updated->getValue()); + } + + public function testUpdateSnippet(): void + { + $snippet = new AiSnippet([ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ]); + $snippet->setDescription('Updated description'); + $snippet->setPlaceholder('%custom:updatedDescription%'); + $snippet->setValue('Updated value.'); + + $this->mockRequestPatch( + '/users/1/ai/settings/snippets/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'description' => 'Updated description', + 'placeholder' => '%custom:updatedDescription%', + 'value' => 'Updated value.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]) + ); + + $updated = $this->crowdin->ai->updateSnippet(1, $snippet); + $this->assertInstanceOf(AiSnippet::class, $updated); + $this->assertEquals('Updated description', $updated->getDescription()); + $this->assertEquals('%custom:updatedDescription%', $updated->getPlaceholder()); + $this->assertEquals('Updated value.', $updated->getValue()); + } } diff --git a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php index b43e941d..9850dd9d 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php @@ -136,6 +136,10 @@ public function testGetFileTranslation(): void $this->assertEquals('50fb3506-4127-4ba8-8296-f97dc7e3e0c3', $fileTranslation->getIdentifier()); $this->assertEquals('finished', $fileTranslation->getStatus()); $this->assertEquals(100, $fileTranslation->getProgress()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getCreatedAt()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getUpdatedAt()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getStartedAt()); + $this->assertEquals('2026-01-23T11:26:54+00:00', $fileTranslation->getFinishedAt()); } public function testDeleteFileTranslation(): void @@ -307,6 +311,15 @@ public function testGetPrompt(): void $this->assertEquals(2, $prompt->getId()); $this->assertTrue($prompt->isFineTuningAvailable()); $this->assertEquals(42, $prompt->getUsageCount()); + $this->assertEquals([1], $prompt->getEnabledProjectIds()); + $this->assertNull($prompt->getPromptPreview()); + $this->assertEquals(123, $prompt->getCreatedBy()); + $this->assertEquals(456, $prompt->getUpdatedBy()); + $this->assertEquals(789, $prompt->getLastUsedBy()); + $this->assertEquals('2019-09-25T14:30:00+00:00', $prompt->getLastUsedAt()); + $this->assertEquals('2019-09-20T11:11:05+00:00', $prompt->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $prompt->getUpdatedAt()); + $this->assertIsArray($prompt->getConfig()); } public function testDeletePrompt(): void @@ -420,6 +433,10 @@ public function testGetPromptCompletion(): void $this->assertInstanceOf(AiPromptCompletion::class, $completion); $this->assertEquals('finished', $completion->getStatus()); $this->assertEquals(100, $completion->getProgress()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getCreatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getUpdatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getStartedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $completion->getFinishedAt()); } public function testDeletePromptCompletion(): void @@ -530,6 +547,11 @@ public function testGetFineTuningDataset(): void $this->assertInstanceOf(AiFineTuningDataset::class, $dataset); $this->assertEquals('finished', $dataset->getStatus()); $this->assertEquals(100, $dataset->getProgress()); + $this->assertIsArray($dataset->getAttributes()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getCreatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getUpdatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getStartedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $dataset->getFinishedAt()); } public function testDownloadFineTuningDataset(): void @@ -628,6 +650,11 @@ public function testGetFineTuningJob(): void $this->assertInstanceOf(AiFineTuningJob::class, $job); $this->assertEquals('finished', $job->getStatus()); $this->assertEquals(100, $job->getProgress()); + $this->assertIsArray($job->getAttributes()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getCreatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getUpdatedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getStartedAt()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $job->getFinishedAt()); } public function testListFineTuningJobs(): void @@ -711,6 +738,8 @@ public function testListFineTuningJobEvents(): void $this->assertInstanceOf(AiFineTuningEvent::class, $events[0]); $this->assertEquals('ftevent-0HOW0hqwQ7G1b70qOC8S7UsT', $events[0]->getId()); $this->assertEquals('Created fine-tuning job: ftjob-EhnfVT9MnddMBLfjMcme71if', $events[0]->getMessage()); + $this->assertEquals('2019-09-23T11:26:54+00:00', $events[0]->getCreatedAt()); + $this->assertIsArray($events[1]->getMetrics()); } public function testListProviders(): void @@ -822,6 +851,11 @@ public function testGetProvider(): void $this->assertEquals(2, $provider->getId()); $this->assertFalse($provider->isUseSystemCredentials()); $this->assertEquals(42, $provider->getPromptsCount()); + $this->assertEquals(['apiKey' => 'string'], $provider->getCredentials()); + $this->assertIsArray($provider->getConfig()); + $this->assertTrue($provider->isEnabled()); + $this->assertEquals('2019-09-20T11:11:05+00:00', $provider->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $provider->getUpdatedAt()); } public function testDeleteProvider(): void @@ -958,6 +992,11 @@ public function testGetReport(): void $this->assertInstanceOf(AiReport::class, $report); $this->assertEquals('finished', $report->getStatus()); $this->assertEquals(100, $report->getProgress()); + $this->assertIsArray($report->getAttributes()); + $this->assertEquals('2024-01-23T11:26:54+00:00', $report->getCreatedAt()); + $this->assertEquals('2024-09-23T11:26:54+00:00', $report->getUpdatedAt()); + $this->assertEquals('2024-05-23T11:26:54+00:00', $report->getStartedAt()); + $this->assertEquals('2024-05-23T11:26:54+00:00', $report->getFinishedAt()); } public function testDownloadReport(): void @@ -1084,6 +1123,8 @@ public function testGetCustomPlaceholder(): void 'The product is the professional consulting service that transform challenges into opportunities.', $placeholder->getValue() ); + $this->assertEquals('2019-09-20T11:11:05+00:00', $placeholder->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $placeholder->getUpdatedAt()); } public function testDeleteCustomPlaceholder(): void @@ -1179,6 +1220,8 @@ public function testGetSnippet(): void 'The product is the professional consulting service that transform challenges into opportunities.', $snippet->getValue() ); + $this->assertEquals('2019-09-20T11:11:05+00:00', $snippet->getCreatedAt()); + $this->assertEquals('2019-09-20T12:22:20+00:00', $snippet->getUpdatedAt()); } public function testDeleteSnippet(): void @@ -1191,4 +1234,212 @@ public function testDeleteSnippet(): void $this->crowdin->ai->deleteSnippet(2); } + + public function testUpdatePrompt(): void + { + $prompt = new AiPrompt([ + 'id' => 2, + 'name' => 'Pre-translate prompt', + 'action' => 'pre_translate', + 'aiProviderId' => 2, + 'aiModelId' => 'gpt-5.4', + 'isEnabled' => true, + 'enabledProjectIds' => [1], + 'config' => [], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ]); + $prompt->setName('Updated prompt'); + $prompt->setAction('translate'); + $prompt->setAiProviderId(3); + $prompt->setAiModelId('gpt-4o'); + $prompt->setEnabledProjectIds([2, 3]); + $prompt->setConfig(['mode' => 'advanced']); + + $this->mockRequestPatch( + '/ai/prompts/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Updated prompt', + 'action' => 'translate', + 'aiProviderId' => 3, + 'aiModelId' => 'gpt-4o', + 'isEnabled' => true, + 'enabledProjectIds' => [2, 3], + 'config' => ['mode' => 'advanced'], + 'promptPreview' => null, + 'isFineTuningAvailable' => true, + 'createdBy' => 123, + 'updatedBy' => 456, + 'lastUsedBy' => 789, + 'lastUsedAt' => '2019-09-25T14:30:00+00:00', + 'usageCount' => 42, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]) + ); + + $updated = $this->crowdin->ai->updatePrompt($prompt); + $this->assertInstanceOf(AiPrompt::class, $updated); + $this->assertEquals(2, $updated->getId()); + $this->assertEquals('Updated prompt', $updated->getName()); + $this->assertEquals('gpt-4o', $updated->getAiModelId()); + } + + public function testUpdateProvider(): void + { + $provider = new AiProvider([ + 'id' => 2, + 'name' => 'OpenAI', + 'type' => 'open_ai', + 'credentials' => ['apiKey' => 'sk-...'], + 'config' => [], + 'isEnabled' => true, + 'useSystemCredentials' => false, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ]); + $provider->setName('Azure OpenAI'); + $provider->setType('azure_open_ai'); + $provider->setCredentials(['apiKey' => 'new-key']); + $provider->setConfig(['actionRules' => []]); + $provider->setIsEnabled(false); + $provider->setUseSystemCredentials(true); + + $this->mockRequestPatch( + '/ai/providers/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'name' => 'Azure OpenAI', + 'type' => 'azure_open_ai', + 'credentials' => ['apiKey' => 'new-key'], + 'config' => ['actionRules' => []], + 'isEnabled' => false, + 'useSystemCredentials' => true, + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + 'promptsCount' => 42, + ], + ]) + ); + + $updated = $this->crowdin->ai->updateProvider($provider); + $this->assertInstanceOf(AiProvider::class, $updated); + $this->assertEquals(2, $updated->getId()); + $this->assertEquals('Azure OpenAI', $updated->getName()); + } + + public function testUpdateSettings(): void + { + $settings = new AiSettings([ + 'preTranslationAiPromptId' => 2, + 'editorSuggestionAiPromptId' => 5, + 'qaCheckActionAiPromptId' => 8, + 'contextReviewAiPromptId' => 11, + ]); + $settings->setPreTranslationAiPromptId(3); + $settings->setEditorSuggestionAiPromptId(6); + $settings->setQaCheckActionAiPromptId(9); + $settings->setContextReviewAiPromptId(12); + + $this->mockRequestPatch( + '/ai/settings', + json_encode([ + 'data' => [ + 'preTranslationAiPromptId' => 3, + 'editorSuggestionAiPromptId' => 6, + 'qaCheckActionAiPromptId' => 9, + 'contextReviewAiPromptId' => 12, + ], + ]) + ); + + $updated = $this->crowdin->ai->updateSettings($settings); + $this->assertInstanceOf(AiSettings::class, $updated); + $this->assertEquals(3, $updated->getPreTranslationAiPromptId()); + $this->assertEquals(6, $updated->getEditorSuggestionAiPromptId()); + $this->assertEquals(9, $updated->getQaCheckActionAiPromptId()); + $this->assertEquals(12, $updated->getContextReviewAiPromptId()); + } + + public function testUpdateCustomPlaceholder(): void + { + $placeholder = new AiCustomPlaceholder([ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ]); + $placeholder->setDescription('Updated description'); + $placeholder->setPlaceholder('%custom:updatedDescription%'); + $placeholder->setValue('Updated value.'); + + $this->mockRequestPatch( + '/ai/settings/custom-placeholders/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'description' => 'Updated description', + 'placeholder' => '%custom:updatedDescription%', + 'value' => 'Updated value.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]) + ); + + $updated = $this->crowdin->ai->updateCustomPlaceholder($placeholder); + $this->assertInstanceOf(AiCustomPlaceholder::class, $updated); + $this->assertEquals('Updated description', $updated->getDescription()); + $this->assertEquals('%custom:updatedDescription%', $updated->getPlaceholder()); + $this->assertEquals('Updated value.', $updated->getValue()); + } + + public function testUpdateSnippet(): void + { + $snippet = new AiSnippet([ + 'id' => 2, + 'description' => 'Product description', + 'placeholder' => '%custom:productDescription%', + 'value' => 'The product is the professional consulting service that transform challenges into opportunities.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ]); + $snippet->setDescription('Updated description'); + $snippet->setPlaceholder('%custom:updatedDescription%'); + $snippet->setValue('Updated value.'); + + $this->mockRequestPatch( + '/ai/settings/snippets/2', + json_encode([ + 'data' => [ + 'id' => 2, + 'description' => 'Updated description', + 'placeholder' => '%custom:updatedDescription%', + 'value' => 'Updated value.', + 'createdAt' => '2019-09-20T11:11:05+00:00', + 'updatedAt' => '2019-09-20T12:22:20+00:00', + ], + ]) + ); + + $updated = $this->crowdin->ai->updateSnippet($snippet); + $this->assertInstanceOf(AiSnippet::class, $updated); + $this->assertEquals('Updated description', $updated->getDescription()); + $this->assertEquals('%custom:updatedDescription%', $updated->getPlaceholder()); + $this->assertEquals('Updated value.', $updated->getValue()); + } } From 80664757f667486b9011df5a4f060d8d79129a34 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Sat, 6 Jun 2026 12:28:27 +0300 Subject: [PATCH 4/4] test: improve coverage --- tests/CrowdinApiClient/Api/AiApiTest.php | 1 + tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/CrowdinApiClient/Api/AiApiTest.php b/tests/CrowdinApiClient/Api/AiApiTest.php index eb77ebd8..e62b1b67 100644 --- a/tests/CrowdinApiClient/Api/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/AiApiTest.php @@ -938,6 +938,7 @@ public function testListProviderModels(): void $this->assertInstanceOf(AiProviderModel::class, $models[0]); $this->assertEquals('gpt-5.4', $models[0]->getId()); $this->assertEquals('open_ai', $models[0]->getProvider()); + $this->assertEquals('OpenAI', $models[0]->getProviderName()); $this->assertEquals(1, $models[0]->getProviderId()); } diff --git a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php index 9850dd9d..f43b0c50 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php @@ -894,6 +894,7 @@ public function testListProviderModels(): void $this->assertInstanceOf(AiProviderModel::class, $models[0]); $this->assertEquals('gpt-5.4', $models[0]->getId()); $this->assertEquals('open_ai', $models[0]->getProvider()); + $this->assertEquals('OpenAI', $models[0]->getProviderName()); } public function testCreateProviderChatCompletion(): void