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
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,21 @@ public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentEx

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
log.error("Unhandled request error", ex);
if (containsProviderException(ex)) {
log.error("Unhandled provider request error type: {}", ex.getClass().getSimpleName());
} else {
log.error("Unhandled request error", ex);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail(5000, "服务处理失败,请查看后端日志"));
}

private boolean containsProviderException(Throwable exception) {
for (Throwable current = exception; current != null; current = current.getCause()) {
if (current instanceof LlmProviderException) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
package com.devflow.copilot.common;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.http.HttpStatus;

@JsonIgnoreProperties({"cause", "stackTrace", "suppressed", "localizedMessage"})
public class LlmProviderException extends BusinessException {

public LlmProviderException(String message) {
super(5001, HttpStatus.BAD_GATEWAY, message);
private final ProviderErrorType errorType;
private final ProviderFailureMetadata failureMetadata;

public LlmProviderException(ProviderErrorType errorType) {
this(errorType, ProviderFailureMetadata.unknown(), null);
}

public LlmProviderException(ProviderErrorType errorType, Throwable cause) {
this(errorType, ProviderFailureMetadata.unknown(), cause);
}

public LlmProviderException(ProviderErrorType errorType, ProviderFailureMetadata failureMetadata) {
this(errorType, failureMetadata, null);
}

public LlmProviderException(ProviderErrorType errorType, ProviderFailureMetadata failureMetadata, Throwable cause) {
super(5001, HttpStatus.BAD_GATEWAY, errorType.safeMessage());
this.errorType = errorType;
this.failureMetadata = failureMetadata == null ? ProviderFailureMetadata.unknown() : failureMetadata;
if (cause != null) {
initCause(cause);
}
}

public ProviderErrorType getErrorType() {
return errorType;
}

public ProviderFailureMetadata getFailureMetadata() {
return failureMetadata;
}

@Override
public String getMessage() {
return errorType.safeMessage();
}

@Override
@JsonIgnore
public synchronized Throwable getCause() {
return super.getCause();
}

public LlmProviderException(String message, Throwable cause) {
super(5001, HttpStatus.BAD_GATEWAY, message);
initCause(cause);
@Override
public String toString() {
return getClass().getName() + ": " + getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.devflow.copilot.common;

public enum ProviderContentTypeCategory {
JSON,
HTML,
TEXT,
OTHER,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.devflow.copilot.common;

public enum ProviderDurationBucket {
UNDER_1_SECOND,
ONE_TO_FIVE_SECONDS,
FIVE_TO_FIFTEEN_SECONDS,
FIFTEEN_TO_SIXTY_SECONDS,
OVER_SIXTY_SECONDS,
TIMEOUT,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.devflow.copilot.common;

public enum ProviderErrorType {
PROTOCOL_UNSUPPORTED("Provider protocol is unsupported."),
API_KEY_MISSING("Provider credential is not configured."),
TIMEOUT("Provider request timed out."),
AUTHENTICATION_FAILED("Provider authentication failed."),
RATE_LIMITED("Provider rate limit reached."),
UPSTREAM_SERVER_ERROR("Provider upstream service failed."),
INVALID_RESPONSE("Provider returned an invalid response."),
EMPTY_CONTENT("Provider returned empty content."),
CONNECTION_FAILED("Provider connection failed."),
UNKNOWN_PROVIDER_ERROR("Provider request failed.");

private final String safeSummary;

ProviderErrorType(String safeSummary) {
this.safeSummary = safeSummary;
}

public String safeMessage() {
return name() + ": " + safeSummary;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.devflow.copilot.common;

import java.util.UUID;

/**
* Deliberately content-free data captured for an external provider failure.
* It must never contain response bodies, URLs, credentials, or header values.
*/
public record ProviderFailureMetadata(
ProviderFailureStage failureStage,
Integer httpStatusCode,
ProviderHttpStatusFamily httpStatusFamily,
Boolean responseBodyPresent,
ProviderResponseSizeBucket responseBodySizeBucket,
Boolean contentTypePresent,
ProviderContentTypeCategory contentTypeCategory,
Boolean retryAfterHeaderPresent,
Boolean upstreamRequestIdHeaderPresent,
String clientRequestId,
ProviderDurationBucket durationBucket
) {

public ProviderFailureMetadata {
failureStage = failureStage == null ? ProviderFailureStage.UNKNOWN : failureStage;
httpStatusFamily = httpStatusFamily == null ? ProviderHttpStatusFamily.UNKNOWN : httpStatusFamily;
responseBodySizeBucket = responseBodySizeBucket == null ? ProviderResponseSizeBucket.UNKNOWN : responseBodySizeBucket;
contentTypeCategory = contentTypeCategory == null ? ProviderContentTypeCategory.UNKNOWN : contentTypeCategory;
durationBucket = durationBucket == null ? ProviderDurationBucket.UNKNOWN : durationBucket;
}

public static ProviderFailureMetadata unknown() {
return new ProviderFailureMetadata(
ProviderFailureStage.UNKNOWN, null, ProviderHttpStatusFamily.UNKNOWN,
null, ProviderResponseSizeBucket.UNKNOWN, null, ProviderContentTypeCategory.UNKNOWN,
null, null, null, ProviderDurationBucket.UNKNOWN
);
}

public static ProviderFailureMetadata atStage(ProviderFailureStage stage) {
return new ProviderFailureMetadata(
stage, null, ProviderHttpStatusFamily.UNKNOWN,
null, ProviderResponseSizeBucket.UNKNOWN, null, ProviderContentTypeCategory.UNKNOWN,
null, null, null, ProviderDurationBucket.UNKNOWN
);
}

public static String newClientRequestId() {
return UUID.randomUUID().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.devflow.copilot.common;

public enum ProviderFailureStage {
CONFIG_VALIDATION,
REQUEST_BUILD,
CONNECTION,
HTTP_STATUS_RECEIVED,
RESPONSE_DESERIALIZATION,
CONTENT_EXTRACTION,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.devflow.copilot.common;

import com.fasterxml.jackson.annotation.JsonValue;

public enum ProviderHttpStatusFamily {
FOUR_XX("4XX"),
FIVE_XX("5XX"),
UNKNOWN("UNKNOWN");

private final String value;

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

@JsonValue
public String value() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.devflow.copilot.common;

public enum ProviderResponseSizeBucket {
ZERO,
ONE_TO_1KB,
ONE_TO_16KB,
OVER_16KB,
UNKNOWN
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
public class AiProviderProperties {

private String provider = "local-rule";
private String baseUrl = "https://api.openai.com/v1";
private String baseUrl = "";
private String apiKey = "";
private String model = "gpt-4.1-mini";
private String protocol = "chat-completions-compatible";
private int timeoutSeconds = 60;
private int maxTokens = 2048;
private boolean fallbackToLocal = true;
Expand All @@ -21,6 +22,8 @@ public class AiProviderProperties {
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public String getProtocol() { return protocol; }
public void setProtocol(String protocol) { this.protocol = protocol; }
public int getTimeoutSeconds() { return timeoutSeconds; }
public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; }
public int getMaxTokens() { return maxTokens; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public class AiGenerateResponse {
private String status;
private String providerName;
private String modelName;
private String requestedProvider;
private String requestedModel;
private String actualProvider;
private String actualModel;
private Boolean fallbackUsed;
private String fallbackReason;
private Long costTimeMs;
private Integer promptTokens;
private Integer completionTokens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ public class GenerationRecord {
private String outputContent;
private GenerationStatus status;
private Boolean confirmed;
private String requestedProvider;
private String requestedModel;
private String providerName;
private String modelName;
private Boolean fallbackUsed;
private String fallbackReason;
private Long promptTemplateId;
private String promptTemplateName;
private Integer promptTemplateVersion;
Expand All @@ -34,6 +38,17 @@ public class GenerationRecord {
private Long costTimeMs;
private Boolean success;
private String errorMessage;
private String providerErrorType;
private String providerFailureStage;
private Integer providerHttpStatus;
private String providerHttpStatusFamily;
private String providerDurationBucket;
private Boolean providerResponseBodyPresent;
private String providerResponseSizeBucket;
private String providerContentTypeCategory;
private Boolean providerRetryAfterPresent;
private Boolean providerRequestIdPresent;
private String providerClientRequestId;
@Version
private Integer version;
private LocalDateTime createdAt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ ToolCallRecord addToolCall(Long runId, Long stepId, String toolName, String inpu

void createPendingReview(Long runId, Long generationRecordId);

void updateExecutionMetadata(Long runId, String providerName, String modelName, Long latencyMs);

void syncGenerationTransition(Long generationRecordId, GenerationStatus target);

List<AgentRun> list(Long projectId, Long generationRecordId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ public void createPendingReview(Long runId, Long generationRecordId) {
humanReviewMapper.insert(review);
}

@Override
public void updateExecutionMetadata(Long runId, String providerName, String modelName, Long latencyMs) {
AgentRun run = runMapper.selectById(runId);
if (run == null) {
return;
}
run.setProviderName(providerName);
run.setModelName(modelName);
run.setLatencyMs(latencyMs);
run.setUpdatedAt(LocalDateTime.now());
runMapper.updateById(run);
}

@Override
@Transactional
public void syncGenerationTransition(Long generationRecordId, GenerationStatus target) {
Expand Down
Loading
Loading