Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b003982fe0 |
@@ -24,6 +24,7 @@ import (
|
||||
aiobject "github.com/hanzoai/ai/object"
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/cloud/clients/admin/core"
|
||||
"github.com/hanzoai/types"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
@@ -174,11 +175,12 @@ func fundingClass(pc ProviderCredit) string {
|
||||
// usage split by funding class over the window (default last 30d). SuperAdmin-guarded.
|
||||
func UsageFunding(s *cloud.Service[core.State], c *zip.Ctx) error {
|
||||
ctx := c.Context()
|
||||
start, end, _, werr := aiobject.ResolveCloudUsageWindow("", c.Query("from"), c.Query("to"), time.Now().UTC())
|
||||
w, werr := types.ParseWindow("", c.Query("from"), c.Query("to"), time.Now().UTC())
|
||||
if werr != nil {
|
||||
end = time.Now().UTC()
|
||||
start = end.AddDate(0, 0, -30)
|
||||
w.End = time.Now().UTC()
|
||||
w.Start = w.End.AddDate(0, 0, -30)
|
||||
}
|
||||
start, end := w.Start, w.End
|
||||
|
||||
cls := map[string]string{}
|
||||
for _, pc := range computeProviderCredits(ctx, s) {
|
||||
|
||||
@@ -65,6 +65,7 @@ import (
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/cloud/clients/principal"
|
||||
"github.com/hanzoai/cloud/clients/sites"
|
||||
"github.com/hanzoai/types"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
@@ -184,18 +185,14 @@ func tenant(c *zip.Ctx) (string, bool) {
|
||||
}
|
||||
|
||||
// window resolves the [start,end) window + bucket interval from ?range/?start/?end,
|
||||
// reusing ai/object.ResolveCloudUsageWindow so analytics and the console Overview
|
||||
// share ONE window grammar (24h|7d|30d|custom). A bad range is a 400.
|
||||
func window(c *zip.Ctx) (time.Time, time.Time, string, string, error) {
|
||||
rangeLabel := strings.TrimSpace(c.Query("range"))
|
||||
start, end, interval, err := aiobject.ResolveCloudUsageWindow(rangeLabel, c.Query("start"), c.Query("end"), time.Now())
|
||||
// reusing types.ParseWindow so analytics and the console Overview share ONE
|
||||
// window grammar (24h|7d|30d|custom). A bad range is a 400.
|
||||
func window(c *zip.Ctx) (time.Time, time.Time, types.Interval, string, error) {
|
||||
w, err := types.ParseWindow(c.Query("range"), c.Query("start"), c.Query("end"), time.Now())
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, "", "", zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if rangeLabel == "" {
|
||||
rangeLabel = "24h"
|
||||
}
|
||||
return start, end, interval, rangeLabel, nil
|
||||
return w.Start, w.End, w.Interval, w.Label, nil
|
||||
}
|
||||
|
||||
// requireDatastore returns the honest 503 when the datastore ledger is not
|
||||
@@ -311,7 +308,7 @@ func overview(s *cloud.Service[state], c *zip.Ctx) error {
|
||||
Range: rangeLabel,
|
||||
Start: start.UTC().Format(time.RFC3339),
|
||||
End: end.UTC().Format(time.RFC3339),
|
||||
Interval: interval,
|
||||
Interval: string(interval),
|
||||
Scope: Scope{Org: org},
|
||||
LLM: llm,
|
||||
Web: buildWebOverview(erow, eventsOK),
|
||||
@@ -338,10 +335,10 @@ func timeseries(s *cloud.Service[state], c *zip.Ctx) error {
|
||||
return zip.Errorf(http.StatusServiceUnavailable, "analytics warehouse unavailable: %v", err)
|
||||
}
|
||||
|
||||
// bucketFn is a CLOSED server-chosen enum (never user input), so interpolating
|
||||
// it is injection-safe; the org + time bounds stay bound parameters.
|
||||
// interval admits only Hour or Day, so interpolating the bucket function is
|
||||
// safe by construction; the org + time bounds stay bound parameters.
|
||||
bucketFn := "Hour"
|
||||
if interval == "day" {
|
||||
if interval == types.Day {
|
||||
bucketFn = "Day"
|
||||
}
|
||||
where, args := llmWhere(org, start, end)
|
||||
@@ -357,7 +354,7 @@ func timeseries(s *cloud.Service[state], c *zip.Ctx) error {
|
||||
Range: rangeLabel,
|
||||
Start: start.UTC().Format(time.RFC3339),
|
||||
End: end.UTC().Format(time.RFC3339),
|
||||
Interval: interval,
|
||||
Interval: string(interval),
|
||||
Scope: Scope{Org: org},
|
||||
Series: buildSeries(start, end, interval, rows),
|
||||
Source: llmTable,
|
||||
|
||||
@@ -28,6 +28,7 @@ package analytics
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/hanzoai/types"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -299,8 +300,8 @@ func buildCommerceOverview(row map[string]any, ok bool) CommerceOverview {
|
||||
// series so the client charts a continuous line. Bucket alignment matches
|
||||
// toStartOf{Hour,Day}(…, 'UTC'): Go's Truncate over the step lands on the same
|
||||
// UTC boundaries. Pure (mirrors ai/object buildCloudUsageSeries).
|
||||
func buildSeries(start, end time.Time, interval string, rows []map[string]any) []SeriesPoint {
|
||||
step := stepOf(interval)
|
||||
func buildSeries(start, end time.Time, interval types.Interval, rows []map[string]any) []SeriesPoint {
|
||||
step := interval.Step()
|
||||
|
||||
type agg struct{ requests, tokens, spend int64 }
|
||||
idx := make(map[int64]agg, len(rows))
|
||||
@@ -326,13 +327,6 @@ func buildSeries(start, end time.Time, interval string, rows []map[string]any) [
|
||||
return out
|
||||
}
|
||||
|
||||
func stepOf(interval string) time.Duration {
|
||||
if strings.EqualFold(interval, "day") {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return time.Hour
|
||||
}
|
||||
|
||||
// buildTopModels assembles the top-models table, computing each model's share of
|
||||
// total spend. Rows arrive already ordered by the query, but we sort defensively
|
||||
// so the pct/ordering is correct regardless of driver row order. Pure.
|
||||
|
||||
+7
-12
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/cloud/clients/commerceinproc"
|
||||
"github.com/hanzoai/cloud/clients/principal"
|
||||
"github.com/hanzoai/types"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
@@ -128,18 +129,15 @@ func summary(s *cloud.Service[state], c *zip.Ctx) error {
|
||||
if !ok {
|
||||
return zip.ErrUnauthorized("sign in to view usage")
|
||||
}
|
||||
rangeLabel := strings.TrimSpace(c.Query("range"))
|
||||
start, end, interval, err := aiobject.ResolveCloudUsageWindow(rangeLabel, c.Query("start"), c.Query("end"), time.Now())
|
||||
w, err := types.ParseWindow(c.Query("range"), c.Query("start"), c.Query("end"), time.Now())
|
||||
if err != nil {
|
||||
return zip.ErrBadRequest(err.Error())
|
||||
}
|
||||
if rangeLabel == "" {
|
||||
rangeLabel = "24h"
|
||||
}
|
||||
rangeLabel, start, end, interval := w.Label, w.Start, w.End, w.Interval
|
||||
ctx := c.Context()
|
||||
|
||||
// ── Spend (commerce) ── best-effort; unconfigured/unreachable → honest zeros.
|
||||
spend := buildSpendBlock(s, ctx, org, start, end, interval)
|
||||
spend := buildSpendBlock(s, ctx, org, start, end, string(interval))
|
||||
|
||||
// ── LLM totals (warehouse) ── honest-empty when the datastore is not connected.
|
||||
llm, warehouseOK := buildLLMBlock(s, ctx, org, start, end)
|
||||
@@ -154,7 +152,7 @@ func summary(s *cloud.Service[state], c *zip.Ctx) error {
|
||||
Range: rangeLabel,
|
||||
Start: start.UTC().Format(time.RFC3339),
|
||||
End: end.UTC().Format(time.RFC3339),
|
||||
Interval: interval,
|
||||
Interval: string(interval),
|
||||
Scope: Scope{Org: org, User: user},
|
||||
Spend: spend,
|
||||
LLM: llm,
|
||||
@@ -216,15 +214,12 @@ func analytics(s *cloud.Service[state], c *zip.Ctx) error {
|
||||
return zip.Errorf(http.StatusPaymentRequired, "analytics datastore is a paid feature — upgrade at console.hanzo.ai")
|
||||
}
|
||||
|
||||
rangeLabel := strings.TrimSpace(c.Query("range"))
|
||||
now := time.Now()
|
||||
start, end, _, werr := aiobject.ResolveCloudUsageWindow(rangeLabel, c.Query("start"), c.Query("end"), now)
|
||||
w, werr := types.ParseWindow(c.Query("range"), c.Query("start"), c.Query("end"), now)
|
||||
if werr != nil {
|
||||
return zip.ErrBadRequest(werr.Error())
|
||||
}
|
||||
if rangeLabel == "" {
|
||||
rangeLabel = "24h"
|
||||
}
|
||||
rangeLabel, start, end := w.Label, w.Start, w.End
|
||||
// Clamp the window to the plan's retention entitlement: a tenant may never read
|
||||
// older than access.RetentionDays, even with a custom ?start.
|
||||
if floor := now.Add(-time.Duration(access.RetentionDays) * 24 * time.Hour); start.Before(floor) {
|
||||
|
||||
@@ -801,6 +801,7 @@ exclude github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83
|
||||
// --- /v1/deploy engine embed (argo gitops-engine, in-process) ---
|
||||
require (
|
||||
github.com/hanzoai/deploy/gitops-engine v0.7.2
|
||||
github.com/hanzoai/types v0.1.0
|
||||
k8s.io/kubernetes v1.35.3 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1150,6 +1150,8 @@ github.com/hanzoai/tasks v1.51.4 h1:qMx80ZfiJPQIDd9lpxxJhDywKYnHOmKxL2IfhZfExgk=
|
||||
github.com/hanzoai/tasks v1.51.4/go.mod h1:+Oqq+L3nY4uKeLyPanimlbcym/HoIUDaBezgIs6v4Hk=
|
||||
github.com/hanzoai/thinking v0.1.1 h1:JuJ9s3q53NRSHSU+/zEdg089vppuNUKK/9BG7bairLw=
|
||||
github.com/hanzoai/thinking v0.1.1/go.mod h1:2lkNDDi+7mAipRhZZ57uqoqrzLKGBdoU5Rb6fGSOoR4=
|
||||
github.com/hanzoai/types v0.1.0 h1:hxPueqy2QnVF3CbvAZ9CJE26pibT/Cb8ZYPhH/CNPwA=
|
||||
github.com/hanzoai/types v0.1.0/go.mod h1:3SEt7J/c5X3C/Uv9n9a+/U9+FGkMQwDSiHwENPFxQW4=
|
||||
github.com/hanzoai/vfs v0.6.6 h1:+4qrHjDOIqgUN1wryU9b5xphDKzTVyBa4CEwbhFSlYw=
|
||||
github.com/hanzoai/vfs v0.6.6/go.mod h1:OiiFWLgrTCMAJimUEgoSy1b/qXHpN34PN3RQwE49e1U=
|
||||
github.com/hanzoai/xorm v1.4.4 h1:2VRwh5BtOgbED+CAzHQ47sPZBgljBlKnJw1Ar6V6il0=
|
||||
|
||||
Reference in New Issue
Block a user