223 lines
9.0 KiB
Go
223 lines
9.0 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
const (
|
|
kiroErrorAuthError = "auth_error"
|
|
kiroErrorMonthlyRequest = "monthly_request_count"
|
|
kiroErrorProfileError = "profile_error"
|
|
kiroErrorQuotaExhausted = "quota_exhausted"
|
|
kiroErrorOverageExhausted = "overage_exhausted"
|
|
kiroErrorRateLimited = "rate_limited"
|
|
kiroErrorSuspended = "suspended"
|
|
kiroErrorUsageForbidden = "usage_forbidden"
|
|
kiroErrorUpstreamTransient = "upstream_transient"
|
|
kiroErrorBadRequestSchema = "bad_request_schema"
|
|
kiroErrorBadRequestToolPairing = "bad_request_tool_pairing"
|
|
kiroErrorBadRequestInvalidModel = "bad_request_invalid_model"
|
|
kiroErrorBadRequestAuth = "bad_request_auth"
|
|
kiroErrorBadRequestQuota = "bad_request_quota"
|
|
kiroErrorBadRequestUnknown = "bad_request_unknown"
|
|
kiroErrorRefreshTokenInvalid = "refresh_token_invalid"
|
|
|
|
kiroQuotaStateNormal = "normal"
|
|
kiroQuotaStateOverageActive = "overage_active"
|
|
kiroQuotaStateCreditsExhausted = "credits_exhausted"
|
|
kiroQuotaStateOverageExhausted = "overage_exhausted"
|
|
)
|
|
|
|
type kiroErrorClassification struct {
|
|
Category string
|
|
StatusCode int
|
|
Message string
|
|
}
|
|
|
|
func classifyKiroHTTPError(statusCode int, body string) kiroErrorClassification {
|
|
trimmed := strings.TrimSpace(body)
|
|
lower := strings.ToLower(trimmed)
|
|
|
|
switch {
|
|
case statusCode == http.StatusUnauthorized:
|
|
return kiroErrorClassification{Category: kiroErrorAuthError, StatusCode: statusCode, Message: trimmed}
|
|
case statusCode == http.StatusPaymentRequired && looksLikeKiroMonthlyRequestCountError(trimmed):
|
|
return kiroErrorClassification{Category: kiroErrorMonthlyRequest, StatusCode: statusCode, Message: trimmed}
|
|
case statusCode == http.StatusForbidden && isKiroSuspendedBody([]byte(trimmed)):
|
|
return kiroErrorClassification{Category: kiroErrorSuspended, StatusCode: statusCode, Message: trimmed}
|
|
case looksLikeKiroProfileError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorProfileError, StatusCode: statusCode, Message: trimmed}
|
|
case statusCode == http.StatusBadRequest:
|
|
return classifyKiroBadRequest(trimmed, lower)
|
|
case statusCode == http.StatusForbidden && isKiroTokenErrorBody([]byte(trimmed)):
|
|
return kiroErrorClassification{Category: kiroErrorAuthError, StatusCode: statusCode, Message: trimmed}
|
|
case looksLikeKiroOverageExhaustedError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorOverageExhausted, StatusCode: statusCode, Message: trimmed}
|
|
case looksLikeKiroQuotaExhaustedError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorQuotaExhausted, StatusCode: statusCode, Message: trimmed}
|
|
case statusCode == http.StatusTooManyRequests:
|
|
return kiroErrorClassification{Category: kiroErrorRateLimited, StatusCode: statusCode, Message: trimmed}
|
|
case statusCode == http.StatusForbidden:
|
|
return kiroErrorClassification{Category: kiroErrorUsageForbidden, StatusCode: statusCode, Message: trimmed}
|
|
case statusCode >= 500:
|
|
return kiroErrorClassification{Category: kiroErrorUpstreamTransient, StatusCode: statusCode, Message: trimmed}
|
|
default:
|
|
return kiroErrorClassification{Category: kiroErrorUpstreamTransient, StatusCode: statusCode, Message: trimmed}
|
|
}
|
|
}
|
|
|
|
func classifyKiroError(err error) kiroErrorClassification {
|
|
if err == nil {
|
|
return kiroErrorClassification{}
|
|
}
|
|
|
|
var httpErr *kiroUsageHTTPError
|
|
if errors.As(err, &httpErr) && httpErr != nil {
|
|
return classifyKiroHTTPError(httpErr.StatusCode, httpErr.Body)
|
|
}
|
|
|
|
errStr := strings.TrimSpace(err.Error())
|
|
lower := strings.ToLower(errStr)
|
|
switch {
|
|
case looksLikeKiroInvalidGrantError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorRefreshTokenInvalid, Message: errStr}
|
|
case looksLikeKiroMonthlyRequestCountError(errStr):
|
|
return kiroErrorClassification{Category: kiroErrorMonthlyRequest, Message: errStr}
|
|
case looksLikeKiroProfileError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorProfileError, Message: errStr}
|
|
case looksLikeKiroOverageExhaustedError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorOverageExhausted, Message: errStr}
|
|
case looksLikeKiroQuotaExhaustedError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorQuotaExhausted, Message: errStr}
|
|
case strings.Contains(lower, "context deadline exceeded"),
|
|
strings.Contains(lower, "timeout"),
|
|
isNetErr(err):
|
|
return kiroErrorClassification{Category: kiroErrorUpstreamTransient, Message: errStr}
|
|
default:
|
|
return kiroErrorClassification{Category: kiroErrorUpstreamTransient, Message: errStr}
|
|
}
|
|
}
|
|
|
|
func classifyKiroBadRequest(trimmed, lower string) kiroErrorClassification {
|
|
switch {
|
|
case looksLikeKiroBadRequestSchemaError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorBadRequestSchema, StatusCode: http.StatusBadRequest, Message: trimmed}
|
|
case looksLikeKiroBadRequestToolPairingError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorBadRequestToolPairing, StatusCode: http.StatusBadRequest, Message: trimmed}
|
|
case looksLikeKiroBadRequestInvalidModelError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorBadRequestInvalidModel, StatusCode: http.StatusBadRequest, Message: trimmed}
|
|
case looksLikeKiroInvalidGrantError(lower) || looksLikeKiroBadRequestAuthError(lower):
|
|
return kiroErrorClassification{Category: kiroErrorBadRequestAuth, StatusCode: http.StatusBadRequest, Message: trimmed}
|
|
case looksLikeKiroQuotaExhaustedError(lower) || looksLikeKiroMonthlyRequestCountError(trimmed):
|
|
return kiroErrorClassification{Category: kiroErrorBadRequestQuota, StatusCode: http.StatusBadRequest, Message: trimmed}
|
|
default:
|
|
return kiroErrorClassification{Category: kiroErrorBadRequestUnknown, StatusCode: http.StatusBadRequest, Message: trimmed}
|
|
}
|
|
}
|
|
|
|
func looksLikeKiroBadRequestSchemaError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(lower, "schema") ||
|
|
strings.Contains(lower, "inputschema") ||
|
|
strings.Contains(lower, "improperly formed request") ||
|
|
strings.Contains(lower, "additionalproperties") ||
|
|
(strings.Contains(lower, "properties") && strings.Contains(lower, "required"))
|
|
}
|
|
|
|
func looksLikeKiroBadRequestToolPairingError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(lower, "tool_use") ||
|
|
strings.Contains(lower, "tool_result") ||
|
|
strings.Contains(lower, "tooluseid") ||
|
|
strings.Contains(lower, "toolresults") ||
|
|
strings.Contains(lower, "must be paired") ||
|
|
strings.Contains(lower, "missing tool result")
|
|
}
|
|
|
|
func looksLikeKiroBadRequestInvalidModelError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(lower, "invalid model") ||
|
|
strings.Contains(lower, "invalid_model_id") ||
|
|
strings.Contains(lower, "model not supported") ||
|
|
strings.Contains(lower, "unsupportedmodel") ||
|
|
strings.Contains(lower, "modelid")
|
|
}
|
|
|
|
func looksLikeKiroBadRequestAuthError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(lower, "invalid token") ||
|
|
strings.Contains(lower, "expired token") ||
|
|
strings.Contains(lower, "access token") ||
|
|
strings.Contains(lower, "refresh token")
|
|
}
|
|
|
|
func looksLikeKiroInvalidGrantError(lower string) bool {
|
|
return strings.Contains(lower, "invalid_grant")
|
|
}
|
|
|
|
func looksLikeKiroMonthlyRequestCountError(body string) bool {
|
|
trimmed := strings.TrimSpace(body)
|
|
if trimmed == "" {
|
|
return false
|
|
}
|
|
if strings.Contains(trimmed, "MONTHLY_REQUEST_COUNT") {
|
|
return true
|
|
}
|
|
if !gjson.Valid(trimmed) {
|
|
return false
|
|
}
|
|
return gjson.Get(trimmed, "reason").String() == "MONTHLY_REQUEST_COUNT" ||
|
|
gjson.Get(trimmed, "error.reason").String() == "MONTHLY_REQUEST_COUNT"
|
|
}
|
|
|
|
func looksLikeKiroProfileError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return (strings.Contains(lower, "profilearn") && strings.Contains(lower, "required")) ||
|
|
(strings.Contains(lower, "profile arn") && strings.Contains(lower, "required")) ||
|
|
(strings.Contains(lower, "profile") && strings.Contains(lower, "not found")) ||
|
|
(strings.Contains(lower, "invalid profile")) ||
|
|
(strings.Contains(lower, "listavailableprofiles"))
|
|
}
|
|
|
|
func looksLikeKiroQuotaExhaustedError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return (strings.Contains(lower, "credit") && (strings.Contains(lower, "exhaust") || strings.Contains(lower, "depleted"))) ||
|
|
(strings.Contains(lower, "quota") && (strings.Contains(lower, "exhaust") || strings.Contains(lower, "exceeded") || strings.Contains(lower, "depleted"))) ||
|
|
(strings.Contains(lower, "usage limit") && (strings.Contains(lower, "reached") || strings.Contains(lower, "exceeded"))) ||
|
|
(strings.Contains(lower, "resource has been exhausted"))
|
|
}
|
|
|
|
func looksLikeKiroOverageExhaustedError(lower string) bool {
|
|
if lower == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(lower, "overage") &&
|
|
(strings.Contains(lower, "exhaust") ||
|
|
strings.Contains(lower, "disabled") ||
|
|
strings.Contains(lower, "not enabled") ||
|
|
strings.Contains(lower, "not allowed") ||
|
|
strings.Contains(lower, "limit"))
|
|
}
|
|
|
|
func isNetErr(err error) bool {
|
|
var netErr net.Error
|
|
return errors.As(err, &netErr)
|
|
}
|