cmd/{analytics,ask,evals,leaderboard,link,rollingcap,usage} each linked
github.com/hanzoai/ai/object, and it cost them 1310-1314 packages against a
671-package core floor. All seven are now 672-677, with ZERO ai packages.
The 638 packages were buying a guaranteed error. aiobject.EnsureCloudUsageTable
execs through object.DatastoreExec, whose connection is opened only by
object.InitDatastore, which runs only inside aimod.Mount — and none of these
seven link the ai module, so the call ALWAYS returned "datastore: not connected"
and every one of the nine call sites silently took its failure branch. The
symptom was an honest-empty dashboard against a warehouse that was up. The next
line at each site already queried clients/datastore, whose connection IS live in
exactly these binaries.
So the DDL moves there — clients/datastore/cloudusage.go, verbatim, guarded by
Ready() and latching only on success, following clients/sbom's ensureTable. It is
deliberately a SECOND copy: ai keeps its own for the write path. A func var the
host injects is the obvious alternative and it is a seam that can never be wired,
because the whole point of these binaries is that they do not link ai. Two copies
of idempotent DDL against one table converge; a nil hook does not.
Two call sites were not the table at all:
clients/answer reached aiobject.Crawl for page reads. clients/websearch already
has a native Crawl4AI client against the same service, and answer already
imports websearch — so Crawl is now exported there and crawl() became its len==1
case. One dial path, one auth path, one decode path, and a duplicate client
gone. Net new packages: zero.
clients/rollingcap read aiobject.TierReader(), and THAT WAS A LIVE BUG: the
rolling AI-spend cap has been dead in every deployment. aiobject's tier reader
is a COPY clients/ai installs at ai.Mount; cloud's is the source, set by
wireTierReader in BuildDeps before MountAll ever runs. cmd/rollingcap never
links clients/ai, and in the unified binary apps.Wire() mounts rollingcap
BEFORE ai — so the copy was nil either way and Mount took its no-op early-out
on every boot. It reads cloud.TierReader() now and the cap is live.
Its other half needed a real seam, so cloud.RollingCapReader joins the four in
ai.go. SetRollingCapReader is the one EXPORTED setter there and the deviation is
forced: the other four are written by build.go/durable.go inside package cloud,
but this producer is clients/rollingcap, which sits above the edge. clients/ai
installs a TRAMPOLINE rather than a snapshot — it resolves the reader per call
— so mount order cannot silence the cap a second time.
Not done here: clients/admin/finance has the same import and is mid-edit by
another change. It is also the one app that would not reach the floor (2184 ->
1612 measured), because it independently pulls part of ai/object's closure.
Measured, CGO_ENABLED=0 go list -deps ./cmd/<app>:
analytics 1311 -> 674 leaderboard 1311 -> 673
ask 1311 -> 675 link 1314 -> 676
evals 1311 -> 673 rollingcap 1310 -> 672
usage 1314 -> 677 ai packages 25 -> 0 (all seven)
Tests: cloud, apps, clients/{rollingcap,usage,analytics,answer,websearch,
leaderboard} all ok. vet clean on the touched set plus ./ and clients/ai.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
54 lines
2.1 KiB
Go
54 lines
2.1 KiB
Go
package cloud
|
|
|
|
import (
|
|
"context"
|
|
|
|
tasksclient "github.com/hanzoai/tasks/pkg/sdk/client"
|
|
)
|
|
|
|
// Callbacks the ai module makes back into the host. Declared here, installed by
|
|
// apps/ — importing ai/object here would put ~1480 packages under every
|
|
// subsystem, and nothing here reads back from it.
|
|
|
|
// UsageEvent mirrors the ai module's payload. Separate on purpose: sharing the
|
|
// type would reintroduce the import.
|
|
type UsageEvent struct {
|
|
Subject string
|
|
Namespace string
|
|
USD string // exact decimal USD ("0.00132"), never a rounded cent
|
|
Currency string
|
|
Model string
|
|
Provider string
|
|
RequestID string
|
|
}
|
|
|
|
type (
|
|
TierReaderFunc func(ctx context.Context, subject, namespace string) (string, error)
|
|
BalanceReaderFunc func(ctx context.Context, subject, namespace, currency string) (int64, error)
|
|
UsageRecorderFunc func(ctx context.Context, u UsageEvent) error
|
|
IngestDialerFunc func(org string) (tasksclient.Client, error)
|
|
RollingCapReaderFunc func(ctx context.Context, subject, namespace string) (bool, error)
|
|
)
|
|
|
|
var (
|
|
tierReader TierReaderFunc
|
|
balanceReader BalanceReaderFunc
|
|
usageRecorder UsageRecorderFunc
|
|
ingestDialer IngestDialerFunc
|
|
rollingCapReader RollingCapReaderFunc
|
|
)
|
|
|
|
// nil means that subsystem isn't co-resident; apps/ leaves it uninstalled.
|
|
func TierReader() TierReaderFunc { return tierReader }
|
|
func BalanceReader() BalanceReaderFunc { return balanceReader }
|
|
func UsageRecorder() UsageRecorderFunc { return usageRecorder }
|
|
func IngestDialer() IngestDialerFunc { return ingestDialer }
|
|
func RollingCapReader() RollingCapReaderFunc { return rollingCapReader }
|
|
|
|
// SetRollingCapReader is the one EXPORTED setter here, and the deviation is
|
|
// forced: the four above are written directly by build.go/durable.go, which are
|
|
// inside this package, but the rolling cap is produced by clients/rollingcap —
|
|
// it imports clients/flags, which imports this package, so it can only ever live
|
|
// above that edge and needs a door. nil clears it (no cap installed).
|
|
func SetRollingCapReader(f RollingCapReaderFunc) { rollingCapReader = f }
|