maestro
View on GitHubmaestro
Maestro is a Go library that provides a shared implementation for tracking usage metrics and sending them to Metronome for billing purposes. It includes HTTP middleware for automatic JWT extraction, context management, and balance checking.
Installation
go get github.com/corticph/gocomo/maestro
Quick Start
1. Initialize the Service
import "github.com/corticph/gocomo/maestro"
service, err := maestro.NewService(
ctx,
"https://api.metronome.com", // Metronome base URL
"your-metronome-token", // Bearer token
"production", // Environment type
nil, // Logger (nil uses slog.Default())
// Optional: override default cache TTLs
maestro.WithBalanceCacheTTL(30 * time.Second), // default 5m
maestro.WithRateCacheTTL(10 * time.Minute), // default 5min
// Optional: on a balance/commit cache miss, allow the request and fetch in the background
// instead of waiting for Metronome. Default true (the first request per customer is not enforced);
// pass false to wait for Metronome on a cache miss.
maestro.WithSkipBalanceCheckOnCacheMiss(false),
)
if err != nil {
// Service is still valid but operates in degraded mode.
// All metering methods no-op and balance checks fail open.
log.Printf("Maestro running in degraded mode: %v", err)
}
On success the service pre-loads the Metronome rate card so that MeterTokens and MeterMinutes can calculate credit costs. If rate loading fails the service is returned in degraded mode — metering calls become no-ops and CheckBalance always allows the request. You can check this state with service.Degraded().
2. Add Billing Context Middleware
mux := http.NewServeMux()
mux.HandleFunc("/api/transcribe", transcribeHandler)
// Wrap your handler with billing context middleware
handler := service.BillingContextMiddleware(mux)
http.ListenAndServe(":8080", handler)
The middleware will:
- Extract JWT from
Authorization: Bearer <token>header - Parse the token and extract claims (without signature verification)
- Extract request metadata (Origin header, Route, RequestMethod)
- Store billing context in the request context
- Continue processing even if token is missing or invalid
3. Add Balance Check Middleware (Optional)
IMPORTANT: The BalanceCheckMiddleware must come AFTER BillingContextMiddleware in your middleware chain, as it depends on billing context being in the request context.
// Chain middlewares in correct order
handler := service.BillingContextMiddleware(
service.BalanceCheckMiddleware()(mux),
)
http.ListenAndServe(":8080", handler)
The balance check middleware will:
- Check the customer's Metronome balance before allowing the request
- Return HTTP 402 Payment Required if balance <= 0
- Fail open (allow request) if:
- No billing context or customer ID found
- Metronome API call fails
- Cache balance results for the specified duration to reduce Metronome API calls
4. Use Metering Methods
All metering methods use CommonEventAttributes which contains common fields from the JWT context and request-specific fields.
MeterSuccessRequest
Track successful API requests (2xx responses). Should be called in middleware after request completion.
err := service.MeterSuccessRequest(ctx, maestro.CommonEventAttributes{
RequestID: "req-123",
MetronomeCustomerID: "customer-456", // Required (auto-populated from JWT if using middleware)
})
MeterTokens
Track LLM token usage. Call after receiving LLM response. Returns UsageInfo with the calculated credit cost.
usageInfo, err := service.MeterTokens(ctx, maestro.TokenEventAttributes{
InputTokens: 150,
OutputTokens: 300,
ModelName: "gpt-4o", // Defaults to "default" if empty
TemplateKey: "chat-template", // Optional
LanguageUsed: "en", // Optional
AgentUsed: "support-agent", // Optional
CommonEventAttributes: maestro.CommonEventAttributes{
RequestID: "req-123",
MetronomeCustomerID: "customer-456", // Required (auto-populated from JWT if using middleware)
},
})
// usageInfo.CreditsConsumed contains the calculated cost
MeterMinutes
Track transcription usage in minutes. Call after transcription completion. Returns UsageInfo with the calculated credit cost.
usageInfo, err := service.MeterMinutes(ctx, maestro.TranscribeEventAttributes{
Minutes: 2.5,
ModelName: "whisper-1", // Defaults to "default" if empty
TemplateKey: "meeting-notes", // Optional
LanguageUsed: "en-US", // Optional
LanguageTier: "premium", // Optional: defaults to "default" if empty
CommonEventAttributes: maestro.CommonEventAttributes{
RequestID: "req-123",
MetronomeCustomerID: "customer-456", // Required (auto-populated from JWT if using middleware)
},
})
// usageInfo.CreditsConsumed contains the calculated cost
Common Attributes
All metering methods accept CommonEventAttributes which can be automatically populated from the billing context (JWT claims + request metadata) if using the BillingContextMiddleware.
CommonEventAttributes Structure
type CommonEventAttributes struct {
// Billing/User Context (auto-populated from JWT if using middleware)
MetronomeCustomerID string // Required: Metronome customer identifier
ApiClientID string // OAuth client ID (from JWT: client_id or azp)
UserEmail string // User's email address (from JWT: email)
ProjectID string // Project identifier (from JWT: project_info.project_id)
Tenant string // Tenant/realm name (extracted from JWT: iss)
// Request Context (auto-populated from request if using middleware)
RequestID string // Required: unique identifier for the request (must be set explicitly)
Route string // API route (e.g., "/v1/chat/completions")
RequestMethod string // HTTP method (e.g., "POST")
Origin string // HTTP Origin header value
// Request Context (auto-populated from x-corti-analytics header/query param)
Analytics Analytics // Optional: dynamic client analytics metadata
}
Note: CommonEventAttributes is used directly for MeterSuccessRequest, while TokenEventAttributes and TranscribeEventAttributes embed it.
Analytics Metadata
Maestro accepts dynamic client analytics metadata received as a single x-corti-analytics header on HTTP requests, or an equivalent x-corti-analytics query parameter during WebSocket handshakes (since browser WebSocket clients may not set custom headers).
The header/query value is a JSON object. For WebSocket clients the JSON value must be URL-encoded when constructing the URL:
x-corti-analytics: {"sdk_client":"corti-sdk-javascript","sdk_version":"1.2.3","web_component":"dictation","web_component_version":"2.4.0","is_console_ai_studio":true}
Attribution metadata is preserved as a dynamic set of keys and included in the corresponding Metronome event properties:
- Fields (
sdk_client,sdk_version,web_component,web_component_version,is_console_ai_studio) are supported but not hard-coded — any key present is passed through. - Partial data is supported; clients do not need to provide every field.
- Unknown fields are ignored rather than failing the request.
- Empty or unavailable fields do not need to be sent.
When using BillingContextMiddleware, the parsed analytics metadata is stored in the billing context and automatically included in metering event properties.
Attribute Population Behavior
The metering methods merge attributes from two sources:
- JWT Context (auto-populated by middleware) - Base attributes from the authenticated user
- Explicit Attributes (provided in method call) - Override context attributes
Priority: Explicit attributes > Context attributes
If not using the middleware, you must provide all common attributes explicitly.
JWT Claims Mapping
The middleware extracts these fields from JWT tokens:
| CommonEventAttribute | JWT Claim | Notes |
|---|---|---|
MetronomeCustomerID |
metronome_customer_id |
Required for all metering calls |
ApiClientID |
client_id or azp |
Falls back to azp if client_id is empty |
UserEmail |
email |
User's email address |
ProjectID |
project_info.project_id |
Nested field in JWT |
Tenant |
iss |
Extracted from issuer URL |
Origin |
(not from JWT) | HTTP Origin request header |
Route |
(not from JWT) | HTTP request path |
RequestMethod |
(not from JWT) | HTTP method |
RequestID |
(not from JWT) | Must be provided explicitly |
Expected JWT Format
{
"iss": "https://auth.example.com/realms/myrealm",
"client_id": "client-123",
"azp": "authorized-party-456",
"email": "user@example.com",
"metronome_customer_id": "customer-789",
"project_info": {
"project_id": "project-001"
}
}
Pricing & Credit Calculation
The service loads a Metronome rate card (named "Usage rate card") at startup and caches pricing by (route, model_name). If a rate is not found in the cache at metering time, it is fetched on demand.
MeterTokens and MeterMinutes return a UsageInfo struct containing CreditsConsumed, calculated from the rate card's conversion factors and prices.
Error Handling
All metering methods return errors that should be logged but typically not block the main request flow:
if err := service.MeterSuccessRequest(ctx, attrs); err != nil {
// Log the error but continue processing
log.Printf("Metering failed: %v", err)
}
The service includes built-in structured logging that will also log failures. The metronome package handles retries for transient errors.