609 lines
18 KiB
Go
609 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
|
kiropkg "github.com/Wei-Shaw/sub2api/internal/pkg/kiro"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
kiroUsageOrigin = "AI_EDITOR"
|
|
kiroUsageResourceType = "AGENTIC_REQUEST"
|
|
|
|
kiroDefaultRegion = "us-east-1"
|
|
)
|
|
|
|
var resolveKiroRuntimeEndpoint = kiroRuntimeEndpoint
|
|
|
|
type kiroUsageLimitsResponse struct {
|
|
NextDateReset any `json:"nextDateReset"`
|
|
OverageConfiguration kiroOverageConfiguration `json:"overageConfiguration"`
|
|
SubscriptionInfo kiroSubscriptionInfo `json:"subscriptionInfo"`
|
|
UsageBreakdownList []kiroUsageBreakdown `json:"usageBreakdownList"`
|
|
}
|
|
|
|
type kiroOverageConfiguration struct {
|
|
OverageStatus string `json:"overageStatus"`
|
|
}
|
|
|
|
type kiroSubscriptionInfo struct {
|
|
SubscriptionTitle string `json:"subscriptionTitle"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type kiroUsageBreakdown struct {
|
|
Currency string `json:"currency"`
|
|
CurrentOverages *float64 `json:"currentOverages"`
|
|
CurrentOveragesWithPrecision *float64 `json:"currentOveragesWithPrecision"`
|
|
CurrentUsage *float64 `json:"currentUsage"`
|
|
CurrentUsageWithPrecision *float64 `json:"currentUsageWithPrecision"`
|
|
DisplayName string `json:"displayName"`
|
|
DisplayNamePlural string `json:"displayNamePlural"`
|
|
FreeTrialInfo *kiroFreeTrialInfo `json:"freeTrialInfo"`
|
|
NextDateReset any `json:"nextDateReset"`
|
|
OverageCharges *float64 `json:"overageCharges"`
|
|
ResourceType string `json:"resourceType"`
|
|
UsageLimit *float64 `json:"usageLimit"`
|
|
UsageLimitWithPrecision *float64 `json:"usageLimitWithPrecision"`
|
|
}
|
|
|
|
type kiroFreeTrialInfo struct {
|
|
CurrentUsage *float64 `json:"currentUsage"`
|
|
CurrentUsageWithPrecision *float64 `json:"currentUsageWithPrecision"`
|
|
FreeTrialExpiry any `json:"freeTrialExpiry"`
|
|
FreeTrialStatus string `json:"freeTrialStatus"`
|
|
UsageLimit *float64 `json:"usageLimit"`
|
|
UsageLimitWithPrecision *float64 `json:"usageLimitWithPrecision"`
|
|
}
|
|
|
|
type kiroUsageHTTPError struct {
|
|
StatusCode int
|
|
Body string
|
|
}
|
|
|
|
func (e *kiroUsageHTTPError) Error() string {
|
|
if e == nil {
|
|
return "kiro usage request failed"
|
|
}
|
|
if strings.TrimSpace(e.Body) == "" {
|
|
return fmt.Sprintf("kiro usage request failed (status %d)", e.StatusCode)
|
|
}
|
|
return fmt.Sprintf("kiro usage request failed (status %d): %s", e.StatusCode, e.Body)
|
|
}
|
|
|
|
func (s *AccountUsageService) getKiroUsage(ctx context.Context, account *Account, source string, forceRefresh bool) (*UsageInfo, error) {
|
|
now := time.Now()
|
|
if account == nil {
|
|
return &UsageInfo{
|
|
Source: source,
|
|
UpdatedAt: &now,
|
|
Error: "account is nil",
|
|
ErrorCode: errorCodeNetworkError,
|
|
}, nil
|
|
}
|
|
if account.Platform != PlatformKiro || account.Type != AccountTypeOAuth {
|
|
return &UsageInfo{
|
|
Source: source,
|
|
UpdatedAt: &now,
|
|
}, nil
|
|
}
|
|
|
|
cached, hasCached := s.getCachedKiroUsage(account.ID)
|
|
if hasCached && (cached.ErrorCode != "" || cached.Error != "") {
|
|
cached.Source = source
|
|
s.attachKiroRuntimeState(ctx, account, cached)
|
|
return cached, nil
|
|
}
|
|
if !forceRefresh && hasCached {
|
|
cached.Source = source
|
|
s.attachKiroRuntimeState(ctx, account, cached)
|
|
return cached, nil
|
|
}
|
|
|
|
flightKey := fmt.Sprintf("kiro-usage:%d", account.ID)
|
|
result, fetchErr, _ := s.cache.kiroUsageFlight.Do(flightKey, func() (any, error) {
|
|
if !forceRefresh {
|
|
if usage, ok := s.getCachedKiroUsage(account.ID); ok {
|
|
return usage, nil
|
|
}
|
|
}
|
|
usage, err := s.fetchAndCacheKiroUsage(ctx, account, source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return usage, nil
|
|
})
|
|
if fetchErr == nil {
|
|
if usage, ok := result.(*UsageInfo); ok && usage != nil {
|
|
usage.Source = source
|
|
s.attachKiroRuntimeState(ctx, account, usage)
|
|
if source == "active" {
|
|
s.tryClearRecoverableAccountError(ctx, account)
|
|
}
|
|
return usage, nil
|
|
}
|
|
}
|
|
|
|
degraded := buildKiroDegradedUsage(fetchErr)
|
|
degraded.Source = source
|
|
if hasCached {
|
|
cached.Error = degraded.Error
|
|
cached.ErrorCode = degraded.ErrorCode
|
|
cached.NeedsReauth = degraded.NeedsReauth
|
|
cached.KiroQuotaState = degraded.KiroQuotaState
|
|
cached.KiroQuotaReason = degraded.KiroQuotaReason
|
|
cached.KiroQuotaResetAt = degraded.KiroQuotaResetAt
|
|
cached.Source = source
|
|
s.attachKiroRuntimeState(ctx, account, cached)
|
|
return cached, nil
|
|
}
|
|
s.storeKiroUsageSnapshot(account.ID, degraded)
|
|
s.attachKiroRuntimeState(ctx, account, degraded)
|
|
return degraded, nil
|
|
}
|
|
|
|
func (s *AccountUsageService) fetchAndCacheKiroUsage(ctx context.Context, account *Account, source string) (*UsageInfo, error) {
|
|
token := strings.TrimSpace(account.GetCredential("access_token"))
|
|
if token == "" {
|
|
return nil, fmt.Errorf("no access token available")
|
|
}
|
|
|
|
region := kiroAPIRegion(account)
|
|
profileArn := strings.TrimSpace(account.GetCredential("profile_arn"))
|
|
|
|
resp, err := s.requestKiroUsageLimits(ctx, account, region, profileArn, token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
usage := mapKiroUsageToInfo(resp)
|
|
usage.Source = source
|
|
s.storeKiroUsageSnapshot(account.ID, usage)
|
|
return usage, nil
|
|
}
|
|
|
|
func (s *AccountUsageService) storeKiroUsageSnapshot(accountID int64, usage *UsageInfo) {
|
|
if s == nil || s.cache == nil || accountID <= 0 || usage == nil {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
if usage.UpdatedAt == nil {
|
|
usage.UpdatedAt = &now
|
|
}
|
|
s.cache.kiroUsageCache.Store(accountID, &kiroUsageCache{
|
|
usageInfo: cloneUsageInfo(usage),
|
|
timestamp: now,
|
|
})
|
|
}
|
|
|
|
func (s *AccountUsageService) getCachedKiroUsage(accountID int64) (*UsageInfo, bool) {
|
|
if s == nil || s.cache == nil || accountID <= 0 {
|
|
return nil, false
|
|
}
|
|
cached, ok := s.cache.kiroUsageCache.Load(accountID)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
cache, ok := cached.(*kiroUsageCache)
|
|
if !ok || cache == nil || cache.usageInfo == nil {
|
|
return nil, false
|
|
}
|
|
if time.Since(cache.timestamp) >= kiroCacheTTL(cache.usageInfo) {
|
|
return nil, false
|
|
}
|
|
return cloneUsageInfo(cache.usageInfo), true
|
|
}
|
|
|
|
func kiroCacheTTL(info *UsageInfo) time.Duration {
|
|
if info == nil {
|
|
return kiroUsageErrorTTL
|
|
}
|
|
if info.ErrorCode != "" || info.Error != "" {
|
|
return kiroUsageErrorTTL
|
|
}
|
|
return apiCacheTTL
|
|
}
|
|
|
|
func cloneUsageInfo(info *UsageInfo) *UsageInfo {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
cloned := *info
|
|
return &cloned
|
|
}
|
|
|
|
func (s *AccountUsageService) requestKiroUsageLimits(ctx context.Context, account *Account, region, profileArn, token string) (*kiroUsageLimitsResponse, error) {
|
|
endpoint := resolveKiroRuntimeEndpoint(region)
|
|
reqURL, err := url.Parse(endpoint + "/getUsageLimits")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build kiro usage url failed: %w", err)
|
|
}
|
|
q := reqURL.Query()
|
|
q.Set("origin", kiroUsageOrigin)
|
|
if profileArn = strings.TrimSpace(profileArn); profileArn != "" {
|
|
q.Set("profileArn", profileArn)
|
|
}
|
|
q.Set("resourceType", kiroUsageResourceType)
|
|
reqURL.RawQuery = q.Encode()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create kiro usage request failed: %w", err)
|
|
}
|
|
s.applyKiroRuntimeHeaders(req, account, token)
|
|
|
|
client, err := httpclient.GetClient(httpclient.Options{
|
|
ProxyURL: accountProxyURL(account),
|
|
Timeout: 30 * time.Second,
|
|
ValidateResolvedIP: true,
|
|
AllowPrivateHosts: isLoopbackEndpoint(endpoint),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create kiro usage client failed: %w", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("kiro usage request failed: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read kiro usage response failed: %w", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, &kiroUsageHTTPError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(body))}
|
|
}
|
|
|
|
var parsed kiroUsageLimitsResponse
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
return nil, fmt.Errorf("decode kiro usage response failed: %w", err)
|
|
}
|
|
return &parsed, nil
|
|
}
|
|
|
|
func (s *AccountUsageService) applyKiroRuntimeHeaders(req *http.Request, account *Account, token string) {
|
|
if req == nil {
|
|
return
|
|
}
|
|
accountKey := buildKiroAccountKey(account)
|
|
machineID := buildKiroMachineID(account)
|
|
req.Header.Set("Accept", "*/*")
|
|
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
|
|
req.Header.Set("User-Agent", kiropkg.BuildRuntimeUserAgent(accountKey, machineID))
|
|
req.Header.Set("X-Amz-User-Agent", kiropkg.BuildRuntimeAmzUserAgent(accountKey, machineID))
|
|
req.Header.Set("x-amzn-kiro-agent-mode", "vibe")
|
|
req.Header.Set("x-amzn-codewhisperer-optout", "true")
|
|
req.Header.Set("Amz-Sdk-Request", "attempt=1; max=3")
|
|
req.Header.Set("Amz-Sdk-Invocation-Id", uuid.NewString())
|
|
|
|
if account == nil {
|
|
return
|
|
}
|
|
applyKiroConditionalHeaders(req, account)
|
|
}
|
|
|
|
func accountProxyURL(account *Account) string {
|
|
if account == nil || account.ProxyID == nil || account.Proxy == nil {
|
|
return ""
|
|
}
|
|
return account.Proxy.URL()
|
|
}
|
|
|
|
func kiroRuntimeEndpoint(region string) string {
|
|
region = strings.TrimSpace(region)
|
|
if region == "" {
|
|
region = kiroDefaultRegion
|
|
}
|
|
switch region {
|
|
case "us-east-1":
|
|
return "https://q.us-east-1.amazonaws.com"
|
|
case "eu-central-1":
|
|
return "https://q.eu-central-1.amazonaws.com"
|
|
case "us-gov-east-1":
|
|
return "https://q-fips.us-gov-east-1.amazonaws.com"
|
|
case "us-gov-west-1":
|
|
return "https://q-fips.us-gov-west-1.amazonaws.com"
|
|
case "us-iso-east-1":
|
|
return "https://q.us-iso-east-1.c2s.ic.gov"
|
|
case "us-isob-east-1":
|
|
return "https://q.us-isob-east-1.sc2s.sgov.gov"
|
|
case "us-isof-south-1":
|
|
return "https://q.us-isof-south-1.csp.hci.ic.gov"
|
|
case "us-isof-east-1":
|
|
return "https://q.us-isof-east-1.csp.hci.ic.gov"
|
|
default:
|
|
if strings.HasPrefix(region, "us-gov-") {
|
|
return "https://q-fips." + region + ".amazonaws.com"
|
|
}
|
|
return "https://q." + region + ".amazonaws.com"
|
|
}
|
|
}
|
|
|
|
func isLoopbackEndpoint(raw string) bool {
|
|
parsed, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
host := strings.TrimSpace(parsed.Hostname())
|
|
if host == "" {
|
|
return false
|
|
}
|
|
if strings.EqualFold(host, "localhost") {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && ip.IsLoopback()
|
|
}
|
|
|
|
func mapKiroUsageToInfo(resp *kiroUsageLimitsResponse) *UsageInfo {
|
|
now := time.Now()
|
|
if resp == nil {
|
|
return &UsageInfo{UpdatedAt: &now}
|
|
}
|
|
info := &UsageInfo{
|
|
UpdatedAt: &now,
|
|
KiroSubscriptionName: strings.TrimSpace(resp.SubscriptionInfo.SubscriptionTitle),
|
|
KiroSubscriptionType: strings.TrimSpace(resp.SubscriptionInfo.Type),
|
|
KiroOveragesEnabled: strings.EqualFold(strings.TrimSpace(resp.OverageConfiguration.OverageStatus), "ENABLED"),
|
|
}
|
|
|
|
resetAt := parseKiroTimestamp(resp.NextDateReset)
|
|
if credit := selectKiroCreditBreakdown(resp.UsageBreakdownList); credit != nil {
|
|
if breakdownReset := parseKiroTimestamp(credit.NextDateReset); breakdownReset != nil {
|
|
resetAt = breakdownReset
|
|
}
|
|
info.KiroCredit = &KiroCreditProgress{
|
|
CurrentUsage: selectKiroFloat(credit.CurrentUsageWithPrecision, credit.CurrentUsage),
|
|
UsageLimit: selectKiroFloat(credit.UsageLimitWithPrecision, credit.UsageLimit),
|
|
PercentageUsed: percentageOrZero(selectKiroFloat(credit.CurrentUsageWithPrecision, credit.CurrentUsage), selectKiroFloat(credit.UsageLimitWithPrecision, credit.UsageLimit)),
|
|
}
|
|
info.KiroOverage = &KiroOverageInfo{
|
|
CurrentOverages: selectKiroFloat(credit.CurrentOveragesWithPrecision, credit.CurrentOverages),
|
|
OverageCharges: selectKiroFloat(credit.OverageCharges, nil),
|
|
CurrencyCode: strings.TrimSpace(credit.Currency),
|
|
CurrencySymbol: kiroCurrencySymbol(strings.TrimSpace(credit.Currency)),
|
|
}
|
|
if ft := credit.FreeTrialInfo; ft != nil && strings.EqualFold(strings.TrimSpace(ft.FreeTrialStatus), "ACTIVE") {
|
|
expiry := parseKiroTimestamp(ft.FreeTrialExpiry)
|
|
daysRemaining := 0
|
|
if expiry != nil {
|
|
daysRemaining = int(time.Until(*expiry).Hours() / 24)
|
|
if time.Until(*expiry)%(24*time.Hour) != 0 {
|
|
daysRemaining++
|
|
}
|
|
if daysRemaining < 0 {
|
|
daysRemaining = 0
|
|
}
|
|
}
|
|
current := selectKiroFloat(ft.CurrentUsageWithPrecision, ft.CurrentUsage)
|
|
limit := selectKiroFloat(ft.UsageLimitWithPrecision, ft.UsageLimit)
|
|
info.KiroBonus = &KiroCreditProgress{
|
|
CurrentUsage: current,
|
|
UsageLimit: limit,
|
|
PercentageUsed: percentageOrZero(current, limit),
|
|
DaysRemaining: daysRemaining,
|
|
ExpiryDate: expiry,
|
|
}
|
|
}
|
|
}
|
|
info.KiroResetAt = resetAt
|
|
setKiroQuotaStateFromUsage(info)
|
|
return info
|
|
}
|
|
|
|
func selectKiroCreditBreakdown(items []kiroUsageBreakdown) *kiroUsageBreakdown {
|
|
for i := range items {
|
|
if strings.EqualFold(strings.TrimSpace(items[i].ResourceType), "CREDIT") {
|
|
return &items[i]
|
|
}
|
|
}
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
return &items[0]
|
|
}
|
|
|
|
func selectKiroFloat(preferred *float64, fallback *float64) float64 {
|
|
switch {
|
|
case preferred != nil:
|
|
return *preferred
|
|
case fallback != nil:
|
|
return *fallback
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func percentageOrZero(current, limit float64) float64 {
|
|
if limit <= 0 {
|
|
return 0
|
|
}
|
|
return current / limit * 100
|
|
}
|
|
|
|
func parseKiroTimestamp(raw any) *time.Time {
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
switch v := raw.(type) {
|
|
case string:
|
|
trimmed := strings.TrimSpace(v)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
if parsed, err := time.Parse(time.RFC3339, trimmed); err == nil {
|
|
return &parsed
|
|
}
|
|
if i, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
|
|
return unixishToTime(i)
|
|
}
|
|
if f, err := strconv.ParseFloat(trimmed, 64); err == nil {
|
|
return unixishFloatToTime(f)
|
|
}
|
|
case float64:
|
|
return unixishFloatToTime(v)
|
|
case int64:
|
|
return unixishToTime(v)
|
|
case int:
|
|
return unixishToTime(int64(v))
|
|
case json.Number:
|
|
if i, err := v.Int64(); err == nil {
|
|
return unixishToTime(i)
|
|
}
|
|
if f, err := v.Float64(); err == nil {
|
|
return unixishFloatToTime(f)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func unixishFloatToTime(v float64) *time.Time {
|
|
if v <= 0 {
|
|
return nil
|
|
}
|
|
if v >= 1e12 {
|
|
t := time.UnixMilli(int64(v))
|
|
return &t
|
|
}
|
|
t := time.Unix(int64(v), 0)
|
|
return &t
|
|
}
|
|
|
|
func unixishToTime(v int64) *time.Time {
|
|
if v <= 0 {
|
|
return nil
|
|
}
|
|
if v >= 1e12 {
|
|
t := time.UnixMilli(v)
|
|
return &t
|
|
}
|
|
t := time.Unix(v, 0)
|
|
return &t
|
|
}
|
|
|
|
func kiroCurrencySymbol(code string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(code)) {
|
|
case "USD":
|
|
return "$"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func buildKiroDegradedUsage(err error) *UsageInfo {
|
|
now := time.Now()
|
|
info := &UsageInfo{
|
|
UpdatedAt: &now,
|
|
Error: "usage API error",
|
|
ErrorCode: errorCodeNetworkError,
|
|
}
|
|
if err == nil {
|
|
return info
|
|
}
|
|
|
|
info.Error = fmt.Sprintf("usage API error: %v", err)
|
|
|
|
classification := classifyKiroError(err)
|
|
switch classification.Category {
|
|
case kiroErrorAuthError:
|
|
info.ErrorCode = errorCodeUnauthenticated
|
|
info.NeedsReauth = true
|
|
case kiroErrorRateLimited:
|
|
info.ErrorCode = errorCodeRateLimited
|
|
case kiroErrorQuotaExhausted:
|
|
info.ErrorCode = errorCodeNetworkError
|
|
info.KiroQuotaState = kiroQuotaStateCreditsExhausted
|
|
info.KiroQuotaReason = classification.Message
|
|
case kiroErrorOverageExhausted:
|
|
info.ErrorCode = errorCodeNetworkError
|
|
info.KiroQuotaState = kiroQuotaStateOverageExhausted
|
|
info.KiroQuotaReason = classification.Message
|
|
case kiroErrorSuspended, kiroErrorUsageForbidden, kiroErrorProfileError:
|
|
info.ErrorCode = errorCodeForbidden
|
|
default:
|
|
info.ErrorCode = errorCodeNetworkError
|
|
}
|
|
return info
|
|
}
|
|
|
|
func (s *AccountUsageService) attachKiroRuntimeState(ctx context.Context, account *Account, usage *UsageInfo) {
|
|
if s == nil || usage == nil || account == nil || account.Platform != PlatformKiro || s.kiroCooldownStore == nil {
|
|
return
|
|
}
|
|
usage.KiroRuntimeState = ""
|
|
usage.KiroRuntimeReason = ""
|
|
usage.KiroRuntimeResetAt = nil
|
|
state, err := s.kiroCooldownStore.GetState(ctx, buildKiroAccountKey(account))
|
|
if err != nil || state == nil {
|
|
return
|
|
}
|
|
usage.KiroRuntimeState, usage.KiroRuntimeReason, usage.KiroRuntimeResetAt = kiroRuntimeStateSnapshot(state)
|
|
}
|
|
|
|
func (s *AccountUsageService) EnrichAccountWithKiroRuntimeState(ctx context.Context, account *Account) {
|
|
if s == nil || account == nil || account.Platform != PlatformKiro || account.Type != AccountTypeOAuth {
|
|
return
|
|
}
|
|
account.KiroQuotaState = ""
|
|
account.KiroQuotaReason = ""
|
|
account.KiroQuotaResetAt = nil
|
|
account.KiroRuntimeState = ""
|
|
account.KiroRuntimeReason = ""
|
|
account.KiroRuntimeResetAt = nil
|
|
if usage, ok := s.getCachedKiroUsage(account.ID); ok {
|
|
account.KiroQuotaState = usage.KiroQuotaState
|
|
account.KiroQuotaReason = usage.KiroQuotaReason
|
|
account.KiroQuotaResetAt = usage.KiroQuotaResetAt
|
|
}
|
|
if s.kiroCooldownStore == nil {
|
|
return
|
|
}
|
|
state, err := s.kiroCooldownStore.GetState(ctx, buildKiroAccountKey(account))
|
|
if err != nil || state == nil {
|
|
return
|
|
}
|
|
account.KiroRuntimeState, account.KiroRuntimeReason, account.KiroRuntimeResetAt = kiroRuntimeStateSnapshot(state)
|
|
}
|
|
|
|
func setKiroQuotaStateFromUsage(info *UsageInfo) {
|
|
if info == nil {
|
|
return
|
|
}
|
|
info.KiroQuotaState = ""
|
|
info.KiroQuotaReason = ""
|
|
info.KiroQuotaResetAt = nil
|
|
|
|
creditExhausted := false
|
|
if info.KiroCredit != nil && info.KiroCredit.UsageLimit > 0 {
|
|
creditExhausted = info.KiroCredit.CurrentUsage >= info.KiroCredit.UsageLimit
|
|
}
|
|
overageActive := info.KiroOverage != nil &&
|
|
(info.KiroOverage.CurrentOverages > 0 || info.KiroOverage.OverageCharges > 0)
|
|
|
|
switch {
|
|
case info.KiroOveragesEnabled && (overageActive || creditExhausted):
|
|
info.KiroQuotaState = kiroQuotaStateOverageActive
|
|
info.KiroQuotaReason = "overages_enabled"
|
|
info.KiroQuotaResetAt = info.KiroResetAt
|
|
case creditExhausted:
|
|
info.KiroQuotaState = kiroQuotaStateCreditsExhausted
|
|
info.KiroQuotaReason = "credits_exhausted"
|
|
info.KiroQuotaResetAt = info.KiroResetAt
|
|
default:
|
|
info.KiroQuotaState = kiroQuotaStateNormal
|
|
}
|
|
}
|