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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/main/java/com/crowdin/client/ai/AIApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class AIApi extends CrowdinApi {

Expand Down Expand Up @@ -423,6 +424,51 @@ public ResponseObject<DownloadLink> downloadAiReport(final Long userId, final St
return ResponseObject.of(response.getData());
}

/**
* @param userId user identifier; should be {@code null} for Crowdin Enterprise
* @param params query parameters
* @return list of AI request logs
* @see <ul>
* <li><a href="https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.getMany" target="_blank"><b>API Documentation</b></a></li>
* <li><a href="https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.getMany" target="_blank"><b>Enterprise API Documentation</b></a></li>
* </ul>
*/
public ResponseList<AiRequestLog> listAiRequestLogs(Long userId, ListAiRequestLogsParams params)
throws HttpException, HttpBadRequestException {
ListAiRequestLogsParams query = Optional.ofNullable(params).orElse(new ListAiRequestLogsParams());

Map<String, Optional<Object>> queryParams = HttpRequestConfig.buildUrlParams(
"requestId", Optional.ofNullable(query.getRequestId()),
"projectId", Optional.ofNullable(query.getProjectId()),
"userId", Optional.ofNullable(query.getUserId()),
"aiProviderId", Optional.ofNullable(query.getAiProviderId()),
"model", Optional.ofNullable(query.getModel()),
"sourceAction", Optional.ofNullable(query.getSourceAction()),
"promptAction", Optional.ofNullable(query.getPromptAction()),
"statuses", Optional.ofNullable(
query.getStatuses() == null ? null : query.getStatuses().stream()
.map(status -> status.to(status))
.collect(Collectors.joining(","))
),
"systemCredentials", Optional.ofNullable(query.getSystemCredentials()),
"isAutoTriggered", Optional.ofNullable(query.getIsAutoTriggered()),
"tokenName", Optional.ofNullable(query.getTokenName()),
"oauthClientId", Optional.ofNullable(query.getOauthClientId()),
"limit", Optional.ofNullable(query.getLimit()),
"offset", Optional.ofNullable(query.getOffset())
);
queryParams.put("createdAfter", Optional.ofNullable(query.getCreatedAfter()));
queryParams.put("createdBefore", Optional.ofNullable(query.getCreatedBefore()));

String url = getAIPath(userId, "ai/request-logs");
AiRequestLogResponseList responseList = this.httpClient.get(
url,
new HttpRequestConfig(queryParams),
AiRequestLogResponseList.class
);
return AiRequestLogResponseList.to(responseList);
}

/**
* @param userId user identifier
* @return AI settings
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/com/crowdin/client/ai/model/AiRequestLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.crowdin.client.ai.model;

import lombok.Data;

import java.util.Date;

@Data
public class AiRequestLog {
private Long id;
private String requestId;
private Date createdAt;
private AiRequestLogStatus status;
private Integer httpStatus;
private String model;
private AiRequestSourceAction sourceAction;
private String promptAction;
private Boolean systemCredentials;
private Boolean isAutoTriggered;
private Integer durationMs;
private Integer inputTokens;
private Integer outputTokens;
private Double totalCost;
private Long userId;
private Long projectId;
private Long promptId;
private Long aiProviderId;
private String tokenName;
private String oauthClientId;
private String oauthClientName;
private String ip;
private String userAgent;
private String error;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.crowdin.client.ai.model;

import com.crowdin.client.core.model.Pagination;
import com.crowdin.client.core.model.ResponseList;
import com.crowdin.client.core.model.ResponseObject;
import lombok.Data;

import java.util.List;
import java.util.stream.Collectors;

@Data
public class AiRequestLogResponseList {
private List<AiRequestLogResponseObject> data;
private Pagination pagination;

public static ResponseList<AiRequestLog> to(AiRequestLogResponseList responseList) {
return ResponseList.of(
responseList.data.stream()
.map(AiRequestLogResponseObject::getData)
.map(ResponseObject::of)
.collect(Collectors.toList()),
responseList.pagination
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.crowdin.client.ai.model;

import lombok.Data;

@Data
public class AiRequestLogResponseObject {
private AiRequestLog data;
}
16 changes: 16 additions & 0 deletions src/main/java/com/crowdin/client/ai/model/AiRequestLogStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.crowdin.client.ai.model;

import com.crowdin.client.core.model.EnumConverter;

public enum AiRequestLogStatus implements EnumConverter<AiRequestLogStatus> {
PENDING, SUCCESS, ERROR, TIMEOUT;

public static AiRequestLogStatus from(String value) {
return AiRequestLogStatus.valueOf(value.toUpperCase());
}

@Override
public String to(AiRequestLogStatus value) {
return value.name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.crowdin.client.ai.model;

import com.crowdin.client.core.model.EnumConverter;

public enum AiRequestSourceAction implements EnumConverter<AiRequestSourceAction> {
AI_PROXY("ai_proxy"),
AI_GATEWAY("ai_gateway"),
AI_TRANSLATE_STRINGS("ai_translate_strings"),
AI_FILE_TRANSLATE("ai_file_translate"),
AI_PROMPT_COMPLETION("ai_prompt_completion"),
PRE_TRANSLATE_MANUAL("pre_translate:manual"),
PRE_TRANSLATE_WORKFLOW("pre_translate:workflow"),
AI_ALIGNMENT("ai_alignment"),
QA_CHECK("qa_check"),
AI_SUGGESTION("ai_suggestion"),
ADVISOR("advisor");

private final String value;

AiRequestSourceAction(String value) {
this.value = value;
}

public static AiRequestSourceAction from(String value) {
for (AiRequestSourceAction sourceAction : values()) {
if (sourceAction.value.equals(value)) {
return sourceAction;
}
}
throw new IllegalArgumentException("Unknown AI request source action: " + value);
}

@Override
public String to(AiRequestSourceAction sourceAction) {
return sourceAction.value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.crowdin.client.ai.model;

import com.crowdin.client.core.model.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.EnumSet;

@EqualsAndHashCode(callSuper = true)
@Data
public class ListAiRequestLogsParams extends Pagination {
private String requestId;
private Long projectId;
private Long userId;
private Long aiProviderId;
private String model;
private AiRequestSourceAction sourceAction;
private String promptAction;
private EnumSet<AiRequestLogStatus> statuses;
private Boolean systemCredentials;
private Boolean isAutoTriggered;
private String tokenName;
private String oauthClientId;
private String createdAfter;
private String createdBefore;
}
67 changes: 67 additions & 0 deletions src/test/java/com/crowdin/client/ai/AIApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public class AIApiTest extends TestClient {
private static final String GENERATE_AI_REPORT_PATH = "%s/users/%d/ai/reports";
private static final String CHECK_AI_REPORT_GENERATION_PATH = "%s/users/%d/ai/reports/%s";
private static final String DOWNLOAD_AI_REPORT_PATH = "%s/users/%d/ai/reports/%s/download";
private static final String USER_AI_REQUEST_LOGS = "%s/users/%d/ai/request-logs";
private static final String ENTERPRISE_AI_REQUEST_LOGS = "%s/ai/request-logs";
private static final String GET_SETTINGS = "%s/users/%d/ai/settings";
private static final String LIST_AI_PROVIDERS = "%s/users/%d/ai/providers";
private static final String GET_AI_PROVIDER = "%s/users/%d/ai/providers/%d";
Expand Down Expand Up @@ -93,6 +95,8 @@ public List<RequestMock> getMocks() {
RequestMock.build(String.format(GENERATE_AI_REPORT_PATH, this.url, userId), HttpPost.METHOD_NAME, "api/ai/generateAiReportRequest.json", "api/ai/generateAiReportResponse.json"),
RequestMock.build(String.format(CHECK_AI_REPORT_GENERATION_PATH, this.url, userId, aiReportId), HttpGet.METHOD_NAME, "api/ai/checkAiReportGenerationStatusResponse.json"),
RequestMock.build(String.format(DOWNLOAD_AI_REPORT_PATH, this.url, userId, aiReportId), HttpGet.METHOD_NAME, "api/ai/downloadAiReportResponse.json"),
RequestMock.build(String.format(USER_AI_REQUEST_LOGS, this.url, userId), HttpGet.METHOD_NAME, "api/ai/listAiRequestLogsResponse.json", aiRequestLogQueryParams()),
RequestMock.build(String.format(ENTERPRISE_AI_REQUEST_LOGS, this.url), HttpGet.METHOD_NAME, "api/ai/listAiRequestLogsResponse.json"),
RequestMock.build(String.format(FINE_TUNING_DATASET_DOWNLOAD_PATH, this.url, userId, aiPromptId, jobIdentifier), HttpGet.METHOD_NAME, "api/ai/downloadFineTuningDataset.json"),
RequestMock.build(String.format(GET_SETTINGS, this.url, userId), HttpGet.METHOD_NAME, "api/ai/getAiSettingResponse.json"),
RequestMock.build(String.format(GET_SETTINGS, this.url, userId), HttpPatch.METHOD_NAME, "api/ai/editAiSettingRequest.json", "api/ai/getAiSettingResponse.json"),
Expand Down Expand Up @@ -326,6 +330,48 @@ public void downloadAiReportTest() {
assertNotNull(responseObject.getData().getUrl());
}

@Test
public void listUserAiRequestLogsTest() {
ListAiRequestLogsParams params = new ListAiRequestLogsParams();
params.setRequestId("9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e");
params.setProjectId(8L);
params.setUserId(42L);
params.setAiProviderId(3L);
params.setModel("gpt-5.6-sol");
params.setSourceAction(AiRequestSourceAction.AI_GATEWAY);
params.setPromptAction("pre_translate");
params.setStatuses(EnumSet.of(AiRequestLogStatus.SUCCESS, AiRequestLogStatus.ERROR));
params.setSystemCredentials(true);
params.setIsAutoTriggered(false);
params.setTokenName("Token name");
params.setOauthClientId("gpbccUFxAKZDrLm5Nq8t");
params.setCreatedAfter("2026-01-01T00:00:00Z");
params.setCreatedBefore("2026-01-02T00:00:00Z");
params.setLimit(limit);
params.setOffset(offset);

ResponseList<AiRequestLog> response = this.getAiApi().listAiRequestLogs(userId, params);

assertEquals(1, response.getData().size());
assertEquals(limit, response.getPagination().getLimit());
assertEquals(offset, response.getPagination().getOffset());
AiRequestLog requestLog = response.getData().get(0).getData();
assertEquals(12345L, requestLog.getId());
assertEquals(AiRequestLogStatus.SUCCESS, requestLog.getStatus());
assertEquals(AiRequestSourceAction.PRE_TRANSLATE_MANUAL, requestLog.getSourceAction());
assertEquals(0.012345, requestLog.getTotalCost());
assertEquals("AI Pipeline", requestLog.getOauthClientName());
assertNull(requestLog.getError());
}

@Test
public void listEnterpriseAiRequestLogsTest() {
ResponseList<AiRequestLog> response = this.getAiApi().listAiRequestLogs(null, null);

assertEquals(1, response.getData().size());
assertEquals(12345L, response.getData().get(0).getData().getId());
}

@Test
public void getAiSettingTest() {
AiSetting aiSetting = this.getAiApi().getAiSetting(userId).getData();
Expand Down Expand Up @@ -662,4 +708,25 @@ public void aiGatewayPatchTest() {
ResponseObject<Map<String, Object>> response = this.getAiApi().aiGatewayPatch(userId, 1L, gatewayPath, req);
assertNotNull(response.getData());
}

private Map<String, Object> aiRequestLogQueryParams() {
Map<String, Object> queryParams = new HashMap<>();
queryParams.put("requestId", "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e");
queryParams.put("projectId", 8L);
queryParams.put("userId", 42L);
queryParams.put("aiProviderId", 3L);
queryParams.put("model", "gpt-5.6-sol");
queryParams.put("sourceAction", AiRequestSourceAction.AI_GATEWAY);
queryParams.put("promptAction", "pre_translate");
queryParams.put("statuses", "success,error");
queryParams.put("systemCredentials", true);
queryParams.put("isAutoTriggered", false);
queryParams.put("tokenName", "Token name");
queryParams.put("oauthClientId", "gpbccUFxAKZDrLm5Nq8t");
queryParams.put("createdAfter", "2026-01-01T00:00:00Z");
queryParams.put("createdBefore", "2026-01-02T00:00:00Z");
queryParams.put("limit", limit);
queryParams.put("offset", offset);
return queryParams;
}
}
36 changes: 36 additions & 0 deletions src/test/resources/api/ai/listAiRequestLogsResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"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": "pre_translate:manual",
"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": "Token name",
"oauthClientId": "gpbccUFxAKZDrLm5Nq8t",
"oauthClientName": "AI Pipeline",
"ip": "203.0.113.42",
"userAgent": "Crowdin Java Client",
"error": null
}
}
],
"pagination": {
"offset": 0,
"limit": 25
}
}
Loading