From c3d3228e508b7057f0bcb5de9cc0b3c35bed3510 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Fri, 11 Sep 2026 21:44:16 +0300 Subject: [PATCH] feat: add support for List AI Request Logs method Adds AiApi::listRequestLogs() (and Enterprise variant) with the AiRequestLog model, closing #279. --- src/CrowdinApiClient/Api/AiApi.php | 32 +++ src/CrowdinApiClient/Api/Enterprise/AiApi.php | 30 +++ src/CrowdinApiClient/Model/AiRequestLog.php | 230 ++++++++++++++++++ tests/CrowdinApiClient/Api/AiApiTest.php | 71 ++++++ .../Api/Enterprise/AiApiTest.php | 71 ++++++ .../Model/AiRequestLogTest.php | 99 ++++++++ 6 files changed, 533 insertions(+) create mode 100644 src/CrowdinApiClient/Model/AiRequestLog.php create mode 100644 tests/CrowdinApiClient/Model/AiRequestLogTest.php diff --git a/src/CrowdinApiClient/Api/AiApi.php b/src/CrowdinApiClient/Api/AiApi.php index cdda4ac7..89c7a61e 100644 --- a/src/CrowdinApiClient/Api/AiApi.php +++ b/src/CrowdinApiClient/Api/AiApi.php @@ -9,6 +9,7 @@ use CrowdinApiClient\Model\AiProviderModel; use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiRequestLog; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; @@ -447,6 +448,37 @@ public function downloadReport(int $userId, string $aiReportId): ?DownloadFile return $this->_get($path, DownloadFile::class); } + /** + * List AI Request Logs + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.getMany API Documentation Enterprise + * + * @param int $userId + * @param array $params + * string $params[requestId] Filter by request identifier
+ * integer $params[projectId] Filter by project
+ * integer $params[userId] Filter by the user attributed to the AI request
+ * integer $params[aiProviderId] Filter by AI provider
+ * string $params[model] Filter by model name
+ * string $params[sourceAction] Filter by the feature or channel that produced the request. Enum: "ai_proxy" "ai_gateway" "ai_translate_strings" "ai_file_translate" "ai_prompt_completion" "pre_translate:manual" "pre_translate:workflow" "ai_alignment" "qa_check" "ai_suggestion" "advisor"
+ * string $params[promptAction] Filter by prompt action
+ * string $params[statuses] Comma-separated request statuses. Enum: "pending" "success" "error" "timeout"
+ * boolean $params[systemCredentials] Filter by whether system-provided credentials were used
+ * boolean $params[isAutoTriggered] Filter by whether the request was triggered automatically
+ * string $params[tokenName] Filter by personal access token name
+ * string $params[oauthClientId] Filter by OAuth client identifier
+ * string $params[createdAfter] Return logs created after this date-time
+ * string $params[createdBefore] Return logs created before this date-time
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listRequestLogs(int $userId, array $params = []): ModelCollection + { + $path = sprintf('users/%d/ai/request-logs', $userId); + return $this->_list($path, AiRequestLog::class, $params); + } + /** * Get AI Settings * @link https://developer.crowdin.com/api/v2/#operation/api.users.ai.settings.get API Documentation diff --git a/src/CrowdinApiClient/Api/Enterprise/AiApi.php b/src/CrowdinApiClient/Api/Enterprise/AiApi.php index a6db555b..0913ae43 100644 --- a/src/CrowdinApiClient/Api/Enterprise/AiApi.php +++ b/src/CrowdinApiClient/Api/Enterprise/AiApi.php @@ -10,6 +10,7 @@ use CrowdinApiClient\Model\AiProviderModel; use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiRequestLog; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; @@ -412,6 +413,35 @@ public function downloadReport(string $aiReportId): ?DownloadFile return $this->_get($path, DownloadFile::class); } + /** + * List AI Request Logs + * @link https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.getMany API Documentation + * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.getMany API Documentation Enterprise + * + * @param array $params + * string $params[requestId] Filter by request identifier
+ * integer $params[projectId] Filter by project
+ * integer $params[userId] Filter by the user attributed to the AI request
+ * integer $params[aiProviderId] Filter by AI provider
+ * string $params[model] Filter by model name
+ * string $params[sourceAction] Filter by the feature or channel that produced the request. Enum: "ai_proxy" "ai_gateway" "ai_translate_strings" "ai_file_translate" "ai_prompt_completion" "pre_translate:manual" "pre_translate:workflow" "ai_alignment" "qa_check" "ai_suggestion" "advisor"
+ * string $params[promptAction] Filter by prompt action
+ * string $params[statuses] Comma-separated request statuses. Enum: "pending" "success" "error" "timeout"
+ * boolean $params[systemCredentials] Filter by whether system-provided credentials were used
+ * boolean $params[isAutoTriggered] Filter by whether the request was triggered automatically
+ * string $params[tokenName] Filter by personal access token name
+ * string $params[oauthClientId] Filter by OAuth client identifier
+ * string $params[createdAfter] Return logs created after this date-time
+ * string $params[createdBefore] Return logs created before this date-time
+ * integer $params[limit]
+ * integer $params[offset] + * @return ModelCollection + */ + public function listRequestLogs(array $params = []): ModelCollection + { + return $this->_list('ai/request-logs', AiRequestLog::class, $params); + } + /** * Get AI Settings * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.settings.get API Documentation diff --git a/src/CrowdinApiClient/Model/AiRequestLog.php b/src/CrowdinApiClient/Model/AiRequestLog.php new file mode 100644 index 00000000..fb69c878 --- /dev/null +++ b/src/CrowdinApiClient/Model/AiRequestLog.php @@ -0,0 +1,230 @@ +id = (int)$this->getDataProperty('id'); + $this->requestId = (string)$this->getDataProperty('requestId'); + $this->createdAt = (string)$this->getDataProperty('createdAt'); + $this->status = (string)$this->getDataProperty('status'); + $this->httpStatus = $this->getDataProperty('httpStatus'); + $this->model = (string)$this->getDataProperty('model'); + $this->sourceAction = (string)$this->getDataProperty('sourceAction'); + $this->promptAction = $this->getDataProperty('promptAction'); + $this->systemCredentials = (bool)$this->getDataProperty('systemCredentials'); + $this->isAutoTriggered = (bool)$this->getDataProperty('isAutoTriggered'); + $this->durationMs = $this->getDataProperty('durationMs'); + $this->inputTokens = $this->getDataProperty('inputTokens'); + $this->outputTokens = $this->getDataProperty('outputTokens'); + $this->totalCost = $this->getDataProperty('totalCost'); + $this->userId = $this->getDataProperty('userId'); + $this->projectId = $this->getDataProperty('projectId'); + $this->promptId = $this->getDataProperty('promptId'); + $this->aiProviderId = (int)$this->getDataProperty('aiProviderId'); + $this->tokenName = $this->getDataProperty('tokenName'); + $this->oauthClientId = $this->getDataProperty('oauthClientId'); + $this->oauthClientName = $this->getDataProperty('oauthClientName'); + $this->ip = $this->getDataProperty('ip'); + $this->userAgent = $this->getDataProperty('userAgent'); + $this->error = $this->getDataProperty('error'); + } + + public function getId(): int + { + return $this->id; + } + + public function getRequestId(): string + { + return $this->requestId; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getHttpStatus(): ?int + { + return $this->httpStatus; + } + + public function getModel(): string + { + return $this->model; + } + + public function getSourceAction(): string + { + return $this->sourceAction; + } + + public function getPromptAction(): ?string + { + return $this->promptAction; + } + + public function isSystemCredentials(): bool + { + return $this->systemCredentials; + } + + public function isAutoTriggered(): bool + { + return $this->isAutoTriggered; + } + + public function getDurationMs(): ?int + { + return $this->durationMs; + } + + public function getInputTokens(): ?int + { + return $this->inputTokens; + } + + public function getOutputTokens(): ?int + { + return $this->outputTokens; + } + + public function getTotalCost(): ?float + { + return $this->totalCost; + } + + public function getUserId(): ?int + { + return $this->userId; + } + + public function getProjectId(): ?int + { + return $this->projectId; + } + + public function getPromptId(): ?int + { + return $this->promptId; + } + + public function getAiProviderId(): int + { + return $this->aiProviderId; + } + + public function getTokenName(): ?string + { + return $this->tokenName; + } + + public function getOauthClientId(): ?string + { + return $this->oauthClientId; + } + + public function getOauthClientName(): ?string + { + return $this->oauthClientName; + } + + public function getIp(): ?string + { + return $this->ip; + } + + public function getUserAgent(): ?string + { + return $this->userAgent; + } + + public function getError(): ?string + { + return $this->error; + } +} diff --git a/tests/CrowdinApiClient/Api/AiApiTest.php b/tests/CrowdinApiClient/Api/AiApiTest.php index 2defe765..22b23299 100644 --- a/tests/CrowdinApiClient/Api/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/AiApiTest.php @@ -9,6 +9,7 @@ use CrowdinApiClient\Model\AiProviderModel; use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiRequestLog; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; @@ -759,6 +760,76 @@ public function testDownloadReport(): void $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); } + public function testListRequestLogs(): void + { + $this->mockRequest([ + 'path' => '/users/1/ai/request-logs?requestId=9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e&projectId=8&userId=42&aiProviderId=3&model=gpt-5.6-sol&sourceAction=ai_gateway&promptAction=pre_translate&statuses=pending%2Csuccess&systemCredentials=1&isAutoTriggered=0&tokenName=CI+token&oauthClientId=gpbccUFxAKZDrLm5Nq8t&createdAfter=2026-01-01T00%3A00%3A00%2B00%3A00&createdBefore=2026-01-02T00%3A00%3A00%2B00%3A00&limit=10&offset=20', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 12345, + 'requestId' => '9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e', + 'createdAt' => '2026-01-01T10:00:00+00:00', + 'status' => 'success', + 'httpStatus' => 200, + 'model' => 'gpt-5.6-sol', + 'sourceAction' => 'ai_gateway', + 'promptAction' => 'pre_translate', + 'systemCredentials' => true, + 'isAutoTriggered' => false, + 'durationMs' => 842, + 'inputTokens' => 512, + 'outputTokens' => 128, + 'totalCost' => 0.012345, + 'userId' => 42, + 'projectId' => 8, + 'promptId' => 5, + 'aiProviderId' => 3, + 'tokenName' => 'CI token', + 'oauthClientId' => 'gpbccUFxAKZDrLm5Nq8t', + 'oauthClientName' => 'AI Pipeline', + 'ip' => '203.0.113.42', + 'userAgent' => 'Mozilla/5.0', + 'error' => null, + ], + ], + ], + 'pagination' => [ + 'offset' => 20, + 'limit' => 10, + ], + ]), + ]); + + $requestLogs = $this->crowdin->ai->listRequestLogs(1, [ + 'requestId' => '9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e', + 'projectId' => 8, + 'userId' => 42, + 'aiProviderId' => 3, + 'model' => 'gpt-5.6-sol', + 'sourceAction' => 'ai_gateway', + 'promptAction' => 'pre_translate', + 'statuses' => 'pending,success', + 'systemCredentials' => true, + 'isAutoTriggered' => false, + 'tokenName' => 'CI token', + 'oauthClientId' => 'gpbccUFxAKZDrLm5Nq8t', + 'createdAfter' => '2026-01-01T00:00:00+00:00', + 'createdBefore' => '2026-01-02T00:00:00+00:00', + 'limit' => 10, + 'offset' => 20, + ]); + + $this->assertInstanceOf(ModelCollection::class, $requestLogs); + $this->assertCount(1, $requestLogs); + $this->assertInstanceOf(AiRequestLog::class, $requestLogs[0]); + $this->assertEquals(12345, $requestLogs[0]->getId()); + $this->assertEquals('success', $requestLogs[0]->getStatus()); + $this->assertEquals(0.012345, $requestLogs[0]->getTotalCost()); + } + public function testGetSettings(): void { $this->mockRequest([ diff --git a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php index 80843165..81b966aa 100644 --- a/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php +++ b/tests/CrowdinApiClient/Api/Enterprise/AiApiTest.php @@ -9,6 +9,7 @@ use CrowdinApiClient\Model\AiProviderModel; use CrowdinApiClient\Model\AiProxyChatCompletion; use CrowdinApiClient\Model\AiReport; +use CrowdinApiClient\Model\AiRequestLog; use CrowdinApiClient\Model\AiSettings; use CrowdinApiClient\Model\AiSnippet; use CrowdinApiClient\Model\AiTranslation; @@ -742,6 +743,76 @@ public function testDownloadReport(): void $this->assertEquals('2019-09-20T10:31:21+00:00', $download->getExpireIn()); } + public function testListRequestLogs(): void + { + $this->mockRequest([ + 'path' => '/ai/request-logs?requestId=9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e&projectId=8&userId=42&aiProviderId=3&model=gpt-5.6-sol&sourceAction=ai_gateway&promptAction=pre_translate&statuses=pending%2Csuccess&systemCredentials=1&isAutoTriggered=0&tokenName=CI+token&oauthClientId=gpbccUFxAKZDrLm5Nq8t&createdAfter=2026-01-01T00%3A00%3A00%2B00%3A00&createdBefore=2026-01-02T00%3A00%3A00%2B00%3A00&limit=10&offset=20', + 'method' => 'get', + 'response' => json_encode([ + 'data' => [ + [ + 'data' => [ + 'id' => 12345, + 'requestId' => '9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e', + 'createdAt' => '2026-01-01T10:00:00+00:00', + 'status' => 'success', + 'httpStatus' => 200, + 'model' => 'gpt-5.6-sol', + 'sourceAction' => 'ai_gateway', + 'promptAction' => 'pre_translate', + 'systemCredentials' => true, + 'isAutoTriggered' => false, + 'durationMs' => 842, + 'inputTokens' => 512, + 'outputTokens' => 128, + 'totalCost' => 0.012345, + 'userId' => 42, + 'projectId' => 8, + 'promptId' => 5, + 'aiProviderId' => 3, + 'tokenName' => 'CI token', + 'oauthClientId' => 'gpbccUFxAKZDrLm5Nq8t', + 'oauthClientName' => 'AI Pipeline', + 'ip' => '203.0.113.42', + 'userAgent' => 'Mozilla/5.0', + 'error' => null, + ], + ], + ], + 'pagination' => [ + 'offset' => 20, + 'limit' => 10, + ], + ]), + ]); + + $requestLogs = $this->crowdin->ai->listRequestLogs([ + 'requestId' => '9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e', + 'projectId' => 8, + 'userId' => 42, + 'aiProviderId' => 3, + 'model' => 'gpt-5.6-sol', + 'sourceAction' => 'ai_gateway', + 'promptAction' => 'pre_translate', + 'statuses' => 'pending,success', + 'systemCredentials' => true, + 'isAutoTriggered' => false, + 'tokenName' => 'CI token', + 'oauthClientId' => 'gpbccUFxAKZDrLm5Nq8t', + 'createdAfter' => '2026-01-01T00:00:00+00:00', + 'createdBefore' => '2026-01-02T00:00:00+00:00', + 'limit' => 10, + 'offset' => 20, + ]); + + $this->assertInstanceOf(ModelCollection::class, $requestLogs); + $this->assertCount(1, $requestLogs); + $this->assertInstanceOf(AiRequestLog::class, $requestLogs[0]); + $this->assertEquals(12345, $requestLogs[0]->getId()); + $this->assertEquals('success', $requestLogs[0]->getStatus()); + $this->assertEquals(0.012345, $requestLogs[0]->getTotalCost()); + } + public function testGetSettings(): void { $this->mockRequest([ diff --git a/tests/CrowdinApiClient/Model/AiRequestLogTest.php b/tests/CrowdinApiClient/Model/AiRequestLogTest.php new file mode 100644 index 00000000..716dadab --- /dev/null +++ b/tests/CrowdinApiClient/Model/AiRequestLogTest.php @@ -0,0 +1,99 @@ + 12345, + 'requestId' => '9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e', + 'createdAt' => '2026-01-01T10:00:00+00:00', + 'status' => 'success', + 'httpStatus' => 200, + 'model' => 'gpt-5.6-sol', + 'sourceAction' => 'ai_gateway', + 'promptAction' => 'pre_translate', + 'systemCredentials' => true, + 'isAutoTriggered' => false, + 'durationMs' => 842, + 'inputTokens' => 512, + 'outputTokens' => 128, + 'totalCost' => 0.012345, + 'userId' => 42, + 'projectId' => 8, + 'promptId' => 5, + 'aiProviderId' => 3, + 'tokenName' => 'CI token', + 'oauthClientId' => 'gpbccUFxAKZDrLm5Nq8t', + 'oauthClientName' => 'AI Pipeline', + 'ip' => '203.0.113.42', + 'userAgent' => 'Mozilla/5.0', + 'error' => null, + ]; + + $requestLog = new AiRequestLog($data); + + $this->assertSame(12345, $requestLog->getId()); + $this->assertSame($data['requestId'], $requestLog->getRequestId()); + $this->assertSame($data['createdAt'], $requestLog->getCreatedAt()); + $this->assertSame($data['status'], $requestLog->getStatus()); + $this->assertSame($data['httpStatus'], $requestLog->getHttpStatus()); + $this->assertSame($data['model'], $requestLog->getModel()); + $this->assertSame($data['sourceAction'], $requestLog->getSourceAction()); + $this->assertSame($data['promptAction'], $requestLog->getPromptAction()); + $this->assertTrue($requestLog->isSystemCredentials()); + $this->assertFalse($requestLog->isAutoTriggered()); + $this->assertSame($data['durationMs'], $requestLog->getDurationMs()); + $this->assertSame($data['inputTokens'], $requestLog->getInputTokens()); + $this->assertSame($data['outputTokens'], $requestLog->getOutputTokens()); + $this->assertSame($data['totalCost'], $requestLog->getTotalCost()); + $this->assertSame($data['userId'], $requestLog->getUserId()); + $this->assertSame($data['projectId'], $requestLog->getProjectId()); + $this->assertSame($data['promptId'], $requestLog->getPromptId()); + $this->assertSame($data['aiProviderId'], $requestLog->getAiProviderId()); + $this->assertSame($data['tokenName'], $requestLog->getTokenName()); + $this->assertSame($data['oauthClientId'], $requestLog->getOauthClientId()); + $this->assertSame($data['oauthClientName'], $requestLog->getOauthClientName()); + $this->assertSame($data['ip'], $requestLog->getIp()); + $this->assertSame($data['userAgent'], $requestLog->getUserAgent()); + $this->assertNull($requestLog->getError()); + } + + public function testLoadDataWithoutOptionalFields(): void + { + $requestLog = new AiRequestLog([ + 'id' => 12345, + 'requestId' => '9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e', + 'createdAt' => '2026-01-01T10:00:00+00:00', + 'status' => 'pending', + 'model' => 'gpt-5.6-sol', + 'sourceAction' => 'ai_gateway', + 'systemCredentials' => false, + 'isAutoTriggered' => true, + 'aiProviderId' => 3, + ]); + + $this->assertNull($requestLog->getHttpStatus()); + $this->assertNull($requestLog->getPromptAction()); + $this->assertNull($requestLog->getDurationMs()); + $this->assertNull($requestLog->getInputTokens()); + $this->assertNull($requestLog->getOutputTokens()); + $this->assertNull($requestLog->getTotalCost()); + $this->assertNull($requestLog->getUserId()); + $this->assertNull($requestLog->getProjectId()); + $this->assertNull($requestLog->getPromptId()); + $this->assertNull($requestLog->getTokenName()); + $this->assertNull($requestLog->getOauthClientId()); + $this->assertNull($requestLog->getOauthClientName()); + $this->assertNull($requestLog->getIp()); + $this->assertNull($requestLog->getUserAgent()); + $this->assertNull($requestLog->getError()); + } +}