From 7253cc5758806aa96c26babf422f7fe4d005dc0b Mon Sep 17 00:00:00 2001 From: Nikhil Date: Fri, 4 Sep 2026 20:23:24 +0530 Subject: [PATCH] feat(ai): add AI request logs support --- .../java/com/crowdin/client/ai/AIApi.java | 46 +++++++++++++ .../crowdin/client/ai/model/AiRequestLog.java | 33 +++++++++ .../ai/model/AiRequestLogResponseList.java | 25 +++++++ .../ai/model/AiRequestLogResponseObject.java | 8 +++ .../client/ai/model/AiRequestLogStatus.java | 16 +++++ .../ai/model/AiRequestSourceAction.java | 37 ++++++++++ .../ai/model/ListAiRequestLogsParams.java | 26 +++++++ .../java/com/crowdin/client/ai/AIApiTest.java | 67 +++++++++++++++++++ .../api/ai/listAiRequestLogsResponse.json | 36 ++++++++++ 9 files changed, 294 insertions(+) create mode 100644 src/main/java/com/crowdin/client/ai/model/AiRequestLog.java create mode 100644 src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseList.java create mode 100644 src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseObject.java create mode 100644 src/main/java/com/crowdin/client/ai/model/AiRequestLogStatus.java create mode 100644 src/main/java/com/crowdin/client/ai/model/AiRequestSourceAction.java create mode 100644 src/main/java/com/crowdin/client/ai/model/ListAiRequestLogsParams.java create mode 100644 src/test/resources/api/ai/listAiRequestLogsResponse.json diff --git a/src/main/java/com/crowdin/client/ai/AIApi.java b/src/main/java/com/crowdin/client/ai/AIApi.java index 76e08b778..450e34e84 100644 --- a/src/main/java/com/crowdin/client/ai/AIApi.java +++ b/src/main/java/com/crowdin/client/ai/AIApi.java @@ -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 { @@ -423,6 +424,51 @@ public ResponseObject 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 + */ + public ResponseList listAiRequestLogs(Long userId, ListAiRequestLogsParams params) + throws HttpException, HttpBadRequestException { + ListAiRequestLogsParams query = Optional.ofNullable(params).orElse(new ListAiRequestLogsParams()); + + Map> 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 diff --git a/src/main/java/com/crowdin/client/ai/model/AiRequestLog.java b/src/main/java/com/crowdin/client/ai/model/AiRequestLog.java new file mode 100644 index 000000000..5f10e08d3 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiRequestLog.java @@ -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; +} diff --git a/src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseList.java b/src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseList.java new file mode 100644 index 000000000..c78760cb1 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseList.java @@ -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 data; + private Pagination pagination; + + public static ResponseList to(AiRequestLogResponseList responseList) { + return ResponseList.of( + responseList.data.stream() + .map(AiRequestLogResponseObject::getData) + .map(ResponseObject::of) + .collect(Collectors.toList()), + responseList.pagination + ); + } +} diff --git a/src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseObject.java b/src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseObject.java new file mode 100644 index 000000000..e0ff50b2c --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiRequestLogResponseObject.java @@ -0,0 +1,8 @@ +package com.crowdin.client.ai.model; + +import lombok.Data; + +@Data +public class AiRequestLogResponseObject { + private AiRequestLog data; +} diff --git a/src/main/java/com/crowdin/client/ai/model/AiRequestLogStatus.java b/src/main/java/com/crowdin/client/ai/model/AiRequestLogStatus.java new file mode 100644 index 000000000..5739b4f28 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiRequestLogStatus.java @@ -0,0 +1,16 @@ +package com.crowdin.client.ai.model; + +import com.crowdin.client.core.model.EnumConverter; + +public enum AiRequestLogStatus implements EnumConverter { + 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(); + } +} diff --git a/src/main/java/com/crowdin/client/ai/model/AiRequestSourceAction.java b/src/main/java/com/crowdin/client/ai/model/AiRequestSourceAction.java new file mode 100644 index 000000000..7695c70ad --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/AiRequestSourceAction.java @@ -0,0 +1,37 @@ +package com.crowdin.client.ai.model; + +import com.crowdin.client.core.model.EnumConverter; + +public enum AiRequestSourceAction implements EnumConverter { + 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; + } +} diff --git a/src/main/java/com/crowdin/client/ai/model/ListAiRequestLogsParams.java b/src/main/java/com/crowdin/client/ai/model/ListAiRequestLogsParams.java new file mode 100644 index 000000000..71f241146 --- /dev/null +++ b/src/main/java/com/crowdin/client/ai/model/ListAiRequestLogsParams.java @@ -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 statuses; + private Boolean systemCredentials; + private Boolean isAutoTriggered; + private String tokenName; + private String oauthClientId; + private String createdAfter; + private String createdBefore; +} diff --git a/src/test/java/com/crowdin/client/ai/AIApiTest.java b/src/test/java/com/crowdin/client/ai/AIApiTest.java index 0e98eb023..9f5cf8f9b 100644 --- a/src/test/java/com/crowdin/client/ai/AIApiTest.java +++ b/src/test/java/com/crowdin/client/ai/AIApiTest.java @@ -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"; @@ -93,6 +95,8 @@ public List 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"), @@ -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 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 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(); @@ -662,4 +708,25 @@ public void aiGatewayPatchTest() { ResponseObject> response = this.getAiApi().aiGatewayPatch(userId, 1L, gatewayPath, req); assertNotNull(response.getData()); } + + private Map aiRequestLogQueryParams() { + Map 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; + } } diff --git a/src/test/resources/api/ai/listAiRequestLogsResponse.json b/src/test/resources/api/ai/listAiRequestLogsResponse.json new file mode 100644 index 000000000..0ba6bfe6f --- /dev/null +++ b/src/test/resources/api/ai/listAiRequestLogsResponse.json @@ -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 + } +}