usage

package
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package usage builds and emits the assistant's billing usage as CloudEvents.

The wire format is BYTE-COMPATIBLE with the TypeScript service's usage emitter (src/usage): the same CloudEvents envelope fields, the same ULID id format, the same meter names, int64-string values, dimensions, subject (projects/{name}), a 100-event batch cap, an optional x-api-key header, a 5s timeout, and never-throw semantics. A recorded golden of the TS wire is diffed against this package's output — do not "improve" the shape.

Index

Constants

View Source
const (
	ResourceGroup = "assistant.miloapis.com"
	ResourceKind  = "Conversation"
)

Group/Kind that emits assistant usage events.

View Source
const (
	MeterInputTokens      = "assistant.miloapis.com/conversation/input-tokens"
	MeterOutputTokens     = "assistant.miloapis.com/conversation/output-tokens"
	MeterCacheReadTokens  = "assistant.miloapis.com/conversation/cache-read-tokens"
	MeterCacheWriteTokens = "assistant.miloapis.com/conversation/cache-write-tokens"
	MeterMessages         = "assistant.miloapis.com/conversation/messages"
	// MeterToolInvocations is a Delta counter: one event per provider-tool
	// invocation, dimensioned by the provider's reverse-DNS service name.
	MeterToolInvocations = "assistant.miloapis.com/conversation/tool-invocations"
)

Canonical meter names (reverse-DNS paths under the service name).

View Source
const ServiceName = "assistant.miloapis.com"

ServiceName is the reverse-DNS service identifier for the assistant.

Variables

This section is empty.

Functions

func IsULID

func IsULID(value string) bool

IsULID reports whether value is a syntactically valid 26-char Crockford ULID.

func NewULID

func NewULID(nowMillis int64) string

NewULID generates a 26-character ULID (10 timestamp chars + 16 randomness chars) for the given unix-millisecond timestamp. It mirrors the TS ulid(): lexicographic ordering and same-millisecond monotonicity are not required (each usage event is an independent dedup key), so the simple form is used.

Types

type BuildToolInvocationInput

type BuildToolInvocationInput struct {
	ProjectName     string
	ConversationID  string
	ConversationUID string
	// ServiceName is the reverse-DNS provider service name (AgentBinding
	// spec.serviceName), e.g. streaming.streamco.example. Emitted as the
	// `service` dimension so billing can price per provider.
	ServiceName string
	Namespace   string // empty defaults to "default"
	NowMillis   int64  // 0 uses time.Now()
}

BuildToolInvocationInput is the input to BuildToolInvocationEvent.

type BuildUsageInput

type BuildUsageInput struct {
	ProjectName     string
	ConversationID  string
	ConversationUID string
	Model           string // Anthropic model id, e.g. claude-sonnet-4-6
	Namespace       string // empty defaults to "default"
	Tokens          UsageTokens
	// NowMillis is the emit time in unix milliseconds; 0 uses time.Now().
	NowMillis int64
}

BuildUsageInput is the input to BuildUsageEvents.

type CloudEvent

type CloudEvent struct {
	ID              string         `json:"id"`
	SpecVersion     string         `json:"specversion"`
	Type            string         `json:"type"`
	Source          string         `json:"source"`
	Subject         string         `json:"subject"`
	DataContentType string         `json:"datacontenttype"`
	Time            string         `json:"time"`
	Data            CloudEventData `json:"data"`
}

CloudEvent is the CloudEvents v1.0 envelope posted to the billing Ingestion Gateway. Field DECLARATION ORDER matches the TS emitter's object-literal order (id, specversion, type, source, subject, datacontenttype, time, data) so a raw byte compare against the TS wire is order-identical; the QA sink normalizer additionally sorts keys, so either way this is byte-compatible.

Gateway-enforced rules: id is a ULID; specversion is "1.0"; subject is projects/{name}; datacontenttype is exactly application/json; data.value is a base-10 int64 string.

func ToCloudEvent

func ToCloudEvent(e Event, source string) CloudEvent

ToCloudEvent converts a service-internal Event into the CloudEvents v1.0 envelope the billing Ingestion Gateway requires. This is the single seam between service-internal data and the platform wire format — every field the gateway validates is set here. Ported verbatim from the TS to-cloud-event.ts.

type CloudEventData

type CloudEventData struct {
	Value      string              `json:"value"`
	Dimensions map[string]string   `json:"dimensions,omitempty"`
	Resource   *CloudEventResource `json:"resource,omitempty"`
}

CloudEventData is the CloudEvents data payload. dimensions and resource are omitted when empty, matching the TS builder's conditional spread.

type CloudEventResource

type CloudEventResource struct {
	Group     string `json:"group"`
	Kind      string `json:"kind"`
	Namespace string `json:"namespace"`
	Name      string `json:"name"`
	UID       string `json:"uid,omitempty"`
}

CloudEventResource is the monitored-resource block; uid is omitted when empty.

type EmitResult

type EmitResult struct {
	// OK is true when the collector accepted the batch (2xx) OR emission was disabled.
	OK bool
	// Noop is true when no collector is configured — events were intentionally dropped.
	Noop bool
	// Count is the number of events submitted (0 when noop or empty).
	Count int
	// Status is the HTTP status of the last request, if one was made.
	Status int
	// Error is a human-readable failure message when OK is false.
	Error string
}

EmitResult reports the outcome of an Emitter.Emit call. It never carries an error return — emission is best-effort and must never fail a chat request.

type Emitter

type Emitter struct {
	// contains filtered or unexported fields
}

Emitter POSTs CloudEvent batches to the in-cluster usage collector (Vector). This is the blessed ingestion path (producer → Vector → gateway → NATS): Vector provides Tier-1 disk durability and injects the gateway api-key, so producers post plaintext with no auth.

func NewEmitter

func NewEmitter(cfg EmitterConfig) *Emitter

NewEmitter constructs an Emitter. When GatewayURL is empty, Emit is a no-op so callers can wire emission unconditionally without breaking local dev.

func (*Emitter) Emit

func (e *Emitter) Emit(ctx context.Context, events []Event) EmitResult

Emit converts events to CloudEvents and POSTs them to <gateway>/cloudevents in batches of at most 100. It never returns an error: any failure is logged and reported via EmitResult. A missing gateway URL is a no-op.

type EmitterConfig

type EmitterConfig struct {
	// GatewayURL is the collector base URL (env USAGE_GATEWAY_URL). Empty ⇒ Emit is a no-op.
	GatewayURL string
	// APIKey is an optional collector api-key (env USAGE_GATEWAY_API_KEY).
	APIKey string
	// Source is the CloudEvents source URI identifying this producer.
	Source string
	// HTTPClient is injectable for tests; nil uses a client with the emit timeout.
	HTTPClient *http.Client
	// Logger is optional; nil discards emitter logs.
	Logger *slog.Logger
}

EmitterConfig configures NewEmitter.

type Event

type Event struct {
	EventID    string
	MeterName  string
	Timestamp  string // ISO-8601
	ProjectRef ProjectRef
	Value      string // numeric value as a string (wire spec)
	Dimensions map[string]string
	Resource   EventResource
}

Event is the service-internal usage event shape (ergonomic for the builders and unit-testable without CloudEvents trivia). It is bridged onto the wire by ToCloudEvent, the only place that knows the CloudEvents envelope.

EventID is the end-to-end dedup key — generated once per logical sample and reused on retry — and must parse as a ULID.

func BuildToolInvocationEvent

func BuildToolInvocationEvent(in BuildToolInvocationInput) Event

BuildToolInvocationEvent builds the single usage event for one provider-tool invocation: value always "1" (a Delta counter aggregated downstream), dimensioned by the provider service. Ported verbatim from the TS builder.

func BuildUsageEvents

func BuildUsageEvents(in BuildUsageInput) []Event

BuildUsageEvents builds one usage Event per non-zero token axis plus the messages meter, each dimensioned by model. It returns an empty slice when no token axis has a positive count.

The messages meter is billed only when the run actually consumed model tokens. A run that failed or was canceled before any model inference produces no tokens and must not be billed a message — it produced no assistant turn. A run that DID consume tokens bills both those tokens and the single message for the interaction that occurred, even if it later failed or was canceled. (This gates the messages axis relative to the TS assistant-events.ts, which emitted it unconditionally.)

type EventResource

type EventResource struct {
	Ref    ResourceRef
	Labels map[string]string
}

EventResource is the full resource block on a usage event: a reference plus a point-in-time descriptive label set.

type ProjectRef

type ProjectRef struct {
	Name string
}

ProjectRef references a Milo project. The pipeline uses it to attribute the event to a BillingAccountBinding.

type ResourceRef

type ResourceRef struct {
	ProjectRef ProjectRef
	Group      string
	Kind       string
	Namespace  string
	Name       string
	UID        string
}

ResourceRef references the resource that emitted the event. Its ProjectRef MUST equal the event's top-level ProjectRef (the gateway rejects mismatches).

type UsageTokens

type UsageTokens struct {
	InputTokens              int64
	OutputTokens             int64
	CachedInputTokens        int64
	CacheCreationInputTokens int64
	// Messages is the per-request message count. nil defaults to 1, matching
	// the TS `tokens.messages ?? 1`.
	Messages *int64
}

UsageTokens carries the per-run token counts surfaced by the model adapter. Any axis at or below zero is skipped (never emitted as a zero-valued event).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL