Documentation
¶
Overview ¶
batchwatch client for Go.
Two rules shape this file.
It never fails upwards. If batchwatch is down, slow or broken, the caller's batch job must not notice. Every submission happens on a background goroutine with a short timeout, and every error is swallowed and handed to an optional debug callback. The only error that ever leaves this package is the caller's own.
It never sends content. Prompts, responses, system prompts, tool calls, file names - none of it. The payload is built from a fixed allowlist of fields: provider, model, mode, endpoint, request count, token counts and timestamps. There is no field to put text in, by construction.
bw := batchwatch.New(batchwatch.Options{Token: "bw_..."})
// 1) before you submit: does this belong in the queue?
if bw.ShouldBatch(ctx, "gpt-5.6-sol", batchwatch.Advice{MaxWait: "15m"}) {
job := client.Batches.Create(...)
}
// 2) measure it
t := bw.Track("gpt-5.6-sol", batchwatch.TrackOpts{InputTokens: batchwatch.Int(9720)})
// ... wait for the job ...
t.Done(batchwatch.DoneOpts{OutputTokens: batchwatch.Int(4519)})
Standard library only - no dependencies.
Package batchwatch is the Go client for batchwatch.dev.
This file is the on-disk spool for measurements that could not be delivered.
A measurement is worth the most precisely when the network is misbehaving, so that is the worst possible moment to drop one. Undeliverable *completed* measurements are appended to a JSONL file and replayed later through POST /v1/calls/complete.
One JSON object per line, in exactly the shape /v1/calls/complete accepts - the same format the Python, TypeScript and .NET clients use, so a file written by one can be flushed by another.
Replay requires an API key: /v1/calls/complete takes the caller's own timestamps and is closed to anonymous callers for that reason. A client without a token therefore does not spool at all - a file nobody can ever send is just a disk leak (see the Batchwatch constructor).
Threads (goroutines) are handled by a mutex; separate processes sharing one spool file are not. Give each process its own BATCHWATCH_SPOOL if that matters.
Index ¶
- Constants
- Variables
- func Int(i int) *int
- func Scrub(body map[string]any) map[string]any
- type Advice
- type Batchwatch
- func (bw *Batchwatch) AdviceResult(ctx context.Context, model string, a Advice) map[string]any
- func (bw *Batchwatch) Flush()
- func (bw *Batchwatch) FlushSpool(ctx context.Context, timeout time.Duration) int
- func (bw *Batchwatch) Measure(model string, opts TrackOpts, fn func(*Tracking) error) error
- func (bw *Batchwatch) ShouldBatch(ctx context.Context, model string, a Advice) bool
- func (bw *Batchwatch) Track(model string, opts TrackOpts) *Tracking
- func (bw *Batchwatch) WaitNow(ctx context.Context, model string, provider, mode string) map[string]any
- type DoneOpts
- type HTTPError
- type Options
- type Record
- type Spool
- type TrackOpts
- type Tracking
Constants ¶
const MaxBatch = 500
MaxBatch is the server's ceiling on one POST /v1/calls/complete.
const MaxBytes int64 = 5 * 1024 * 1024
MaxBytes caps the spool file. If batchwatch is down for a week, a busy pipeline must not fill the user's disk. Past the cap the measurement is dropped - his machine is not our storage.
const Version = "0.2.1"
Version of the client, sent in the user-agent.
Variables ¶
var AllowedFields = map[string]bool{ "provider": true, "model": true, "mode": true, "endpoint": true, "requests": true, "input_tokens": true, "output_tokens": true, "started_at": true, "ended_at": true, "status": true, "ttfb_ms": true, "source": true, "acted_verdict": true, "deadline_s": true, "quoted_p50_s": true, "quoted_p90_s": true, }
AllowedFields are the only keys that may leave the machine. Everything else does not exist in the body. The test no_content_test.go pins this list.
This is the Go twin of Scrub; the same seven-plus-source allowlist as the Python and TypeScript clients.
The last four (acted_verdict, deadline_s, quoted_p50_s, quoted_p90_s) are the verdict-accuracy measurement (#101): the advice we ourselves gave, attached to the later completion so the SERVER can compare its measured duration to what we quoted. All numbers and a decision we made - no new PII.
Functions ¶
func Int ¶
Int returns a pointer to i. Use it for token counts: a nil *int means "unknown" and is sent as null; Int(0) sends 0, because zero is a measurement and absence is not.
Types ¶
type Advice ¶
type Advice struct {
MaxWait string
Provider string
Risk string
// InputTokens, OutputTokens and MaxTokens are omitted when nil. Output
// tokens are almost always UNKNOWN before the model answers - leave it
// nil. Sending 0 would make the server price the saving on zero output,
// which is systematically too low.
InputTokens *int
OutputTokens *int
MaxTokens *int
// Fallback is returned by ShouldBatch when we cannot answer. Default
// false = "run it synchronously".
Fallback bool
}
Advice is the query for ShouldBatch / Advice. Zero-valued fields are omitted from the request; use Int() for the token pointers so that "unknown" (nil) is distinguishable from an explicit value.
type Batchwatch ¶
type Batchwatch struct {
Token string
Base string
Timeout time.Duration
Enabled bool
Spool *Spool
// contains filtered or unexported fields
}
Batchwatch is the client. Every submission is non-blocking and fails open.
func (*Batchwatch) AdviceResult ¶
AdviceResult asks the whole verdict. Returns nil if we cannot answer.
OutputTokens is usually UNKNOWN at this point - the model decides them. It is left out unless you pass it. Sending zero would make the server compute the saving on zero output, and output costs five to six times as much as input: the answer would be systematically too low, with nobody able to see it. If you know a ceiling, pass MaxTokens.
func (*Batchwatch) Flush ¶
func (bw *Batchwatch) Flush()
Flush waits for outstanding submissions. Call it before the process exits.
func (*Batchwatch) FlushSpool ¶
FlushSpool sends everything waiting on disk. Returns the number accepted.
Never returns an error. Records the server rejects as invalid are dropped - they will never become valid.
func (*Batchwatch) Measure ¶
Measure runs fn and closes the measurement for you, as failed if fn returns an error - and the error is returned untouched.
func (*Batchwatch) ShouldBatch ¶
ShouldBatch answers true/false. On any doubt you get a.Fallback back.
We never guess on the caller's behalf. The default (Fallback=false) is "run it synchronously", which is the safe way to be wrong: a synchronous call merely costs more, while an unexpected eight-hour queue can take down a product.
type DoneOpts ¶
type DoneOpts struct {
// OutputTokens stays null when nil. It is never defaulted to zero: zero
// is a measurement, absence is not, and the server prices them
// differently on purpose. Use Int(0) to send an explicit zero.
OutputTokens *int
// Status defaults to "completed". Others: failed, expired, cancelled,
// abandoned.
Status string
// TTFBMs is sent only when non-nil.
TTFBMs *int
}
DoneOpts close a measurement. OutputTokens is a *int: leave it nil when you do not know it and it is sent as null, never 0.
type HTTPError ¶
HTTPError is a non-2xx response. It never reaches the caller of a public method - only the debug OnError callback.
type Options ¶
type Options struct {
// Token is the API key. Falls back to $BATCHWATCH_TOKEN. Optional for
// measuring, required for replaying a spool.
Token string
// BaseURL defaults to $BATCHWATCH_URL or https://batchwatch.dev.
BaseURL string
// Timeout is the per-HTTP-call deadline. Defaults to $BATCHWATCH_TIMEOUT
// seconds or 2s.
Timeout time.Duration
// Enabled=false turns every network call into a no-op. The zero value of
// Options leaves this false, so use New(), which defaults it to true;
// set DisableEnabled to force it off explicitly.
Enabled *bool
// Spool is the spool file path. Empty string uses the default location
// ($BATCHWATCH_SPOOL or a temp file); set SpoolDisabled to turn it off.
Spool string
// SpoolDisabled turns spooling off regardless of Spool.
SpoolDisabled bool
// OnError is called on every swallowed error. Wire it to your logger at
// debug level; nothing is printed otherwise.
OnError func(error)
// HTTPClient lets you inject a custom client (proxies, transport). The
// per-call timeout is applied via context regardless.
HTTPClient *http.Client
}
Options configure a Batchwatch client. The zero value is usable; every field falls back to an environment variable or a sensible default.
type Record ¶
Record is one measurement, in the shape /v1/calls/complete accepts. Using json.RawMessage-free maps keeps the format byte-identical to the other clients, which write plain JSON objects.
type Spool ¶
type Spool struct {
// Path is the spool file. Pending is Path + ".pending".
Path string
Pending string
// MaxBytes is the file-size ceiling. Defaults to the package MaxBytes.
MaxBytes int64
// OnError fires on every swallowed error. Wire it to your logger at
// debug level; nothing is printed otherwise.
OnError func(error)
// contains filtered or unexported fields
}
Spool is an append-only JSONL file of completed measurements awaiting delivery. Every method is safe under concurrent goroutines and none of them ever returns the caller an error that would matter: failing to spool must not be worse than the network failure that caused it.
func (*Spool) Append ¶
Append stores one completed measurement. Returns true if it was stored.
Never returns an error to the caller: failing to spool must not be worse than the network failure that caused the spooling.
type TrackOpts ¶
type TrackOpts struct {
Provider string
Mode string
Requests *int
InputTokens *int
Endpoint string
}
TrackOpts configure a measurement. InputTokens is a *int so an unknown count stays null on the wire.
type Tracking ¶
type Tracking struct {
// contains filtered or unexported fields
}
Tracking is the handle for one measurement in flight. Returned by Track().
A Tracking is safe to Done() from any goroutine; Done() is idempotent.
func (*Tracking) Done ¶
Done closes the measurement. Safe to call more than once; only the first call does anything.