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());
+ }
+}