package status import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "math" "sort" "strconv" "strings" "time" ) const quotaSnapshotSchemaVersion = "1.0" // QuotaCapView is the normalized, non-sensitive evidence for one required // usage cap. It deliberately excludes UsageStatus.RawOutput. type QuotaCapView struct { Name string `json:"name"` Status string `json:"status"` RemainingPercent *float64 `json:"remaining_percent"` } // QuotaTargetView is the selector-facing availability result for one target. type QuotaTargetView struct { Adapter string `json:"adapter"` Target string `json:"target"` Status string `json:"status"` } // QuotaSnapshot is a narrow JSON bridge from the Go usage checker to the // execution selector. It never serializes provider output or checker errors. type QuotaSnapshot struct { SchemaVersion string `json:"schema_version"` SnapshotID string `json:"snapshot_id"` Source string `json:"source"` CheckedAt string `json:"checked_at"` Targets []QuotaTargetView `json:"targets"` RequiredCaps []QuotaCapView `json:"required_caps"` ReasonCodes []string `json:"reason_codes"` } // NormalizeQuotaSnapshot resolves the declared cap set against a parsed usage // status. A confirmed exhausted cap wins over missing or malformed evidence so // callers do not admit a target known to be exhausted. func NormalizeQuotaSnapshot(adapter, target string, requiredCaps []string, checkedAt time.Time, usage *UsageStatus, checkErr error) QuotaSnapshot { views := make([]QuotaCapView, 0, len(requiredCaps)) reasons := make([]string, 0, 1) if checkErr != nil || usage == nil { for _, cap := range requiredCaps { views = append(views, QuotaCapView{Name: cap, Status: "unknown"}) } reasons = append(reasons, "checker_error") } else { for _, cap := range requiredCaps { views = append(views, resolveQuotaCap(usage, cap)) } } status := "available" for _, view := range views { if view.Status == "exhausted" { status = "exhausted" break } if view.Status != "available" { status = "unknown" } } if len(views) == 0 { status = "unknown" reasons = append(reasons, "required_cap_missing") } if status == "unknown" && len(reasons) == 0 { reasons = append(reasons, "cap_evidence_unknown") } checked := checkedAt.UTC().Format(time.RFC3339Nano) snapshot := QuotaSnapshot{ SchemaVersion: quotaSnapshotSchemaVersion, Source: "iop-node quota-probe", CheckedAt: checked, Targets: []QuotaTargetView{{Adapter: adapter, Target: target, Status: status}}, RequiredCaps: views, ReasonCodes: reasons, } snapshot.SnapshotID = quotaSnapshotID(snapshot) return snapshot } func resolveQuotaCap(usage *UsageStatus, required string) QuotaCapView { view := QuotaCapView{Name: required, Status: "unknown"} if usage == nil { return view } if required == "overall" { return quotaCapFromRemaining(required, usage.DailyLimit) } model, ok := strings.CutPrefix(required, "model:") if !ok || strings.TrimSpace(model) == "" { return view } count, err := strconv.Atoi(usage.Metadata["model_usage_count"]) if err != nil || count < 0 { return view } matches := make([]string, 0, 1) for index := 0; index < count; index++ { prefix := fmt.Sprintf("model_usage_%d", index) if strings.EqualFold(strings.TrimSpace(usage.Metadata[prefix+"_name"]), strings.TrimSpace(model)) { matches = append(matches, usage.Metadata[prefix+"_used_percent"]) } } if len(matches) != 1 { return view } used, ok := parsePercent(matches[0]) if !ok { return view } remaining := 100 - used return quotaCapFromNumber(required, remaining) } func quotaCapFromRemaining(name, value string) QuotaCapView { remaining, ok := parsePercent(value) if !ok { return QuotaCapView{Name: name, Status: "unknown"} } return quotaCapFromNumber(name, remaining) } func quotaCapFromNumber(name string, remaining float64) QuotaCapView { view := QuotaCapView{Name: name, RemainingPercent: &remaining} if remaining == 0 { view.Status = "exhausted" } else { view.Status = "available" } return view } func parsePercent(value string) (float64, bool) { value = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(value), "%")) if value == "" { return 0, false } parsed, err := strconv.ParseFloat(value, 64) if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed < 0 || parsed > 100 { return 0, false } return parsed, true } func quotaSnapshotID(snapshot QuotaSnapshot) string { caps := append([]QuotaCapView(nil), snapshot.RequiredCaps...) sort.Slice(caps, func(i, j int) bool { return caps[i].Name < caps[j].Name }) payload := struct { CheckedAt string `json:"checked_at"` Target QuotaTargetView `json:"target"` Caps []QuotaCapView `json:"caps"` }{ CheckedAt: snapshot.CheckedAt, Target: snapshot.Targets[0], Caps: caps, } encoded, _ := json.Marshal(payload) digest := sha256.Sum256(encoded) return "quota-" + hex.EncodeToString(digest[:]) }