eval

package
v1.801.384 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package eval is scoring a model on your own data, with a judge you choose.

/v1/evals: datasets, dataset items, evaluators, score configs, runs, scores and traces, per org. Native, and nothing proxies to the retired observability console.

Storage split (CTO directive), two orthogonal stores this package composes:

  • metastore (store.go) — Hanzo Base/SQLite, per-org config/metadata: datasets, dataset items, evaluators, rubrics, dataset-run defs.
  • telemetry (telemetry.go) — apps/datastore, append-only event stream: traces + scores-as-events (all AI observability). Optional at Mount; when no datastore is wired the run still scores, but trace/score persistence is honestly skipped (logged), never faked.

The run orchestrator drives a REAL evaluation: for each ACTIVE dataset item it (1) calls the model-under-test through the runner, (2) records a trace, (3) calls the LLM-as-judge through the runner, (4) validates + records the score, (5) updates the durable run record. The runner is PLUGGABLE (EvalRunner): today an in-process gateway runner (the caller's bearer → loopback /v1/chat/completions); a DigitalOcean-backed runner can drop in behind the same interface without touching the store/API/FE.

Tenant isolation is enforced SERVER-SIDE on every request: the org is c.Org() — the value SanitizeIdentity minted from the VALIDATED bearer owner (HIP-0026) — and NEVER a client-supplied X-Org-Id/X-Project-Id header. Every metastore query filters WHERE org=?; every telemetry read binds org as a named parameter.

Surface (all org-scoped; /v1 only):

POST   /v1/evals/datasets         create/upsert a dataset            -> Dataset
GET    /v1/evals/datasets         list the org's datasets            -> {data:[…]}
GET    /v1/evals/datasets/:name   dataset detail + item count        -> Dataset
DELETE /v1/evals/datasets/:name   delete a dataset (+ its items)
POST   /v1/evals/datasets/:name/items  create/upsert an item         -> DatasetItem
GET    /v1/evals/datasets/:name/items  list a dataset's items (limit) -> {data:[…]}
POST   /v1/evals/evaluators       create/upsert an evaluator         -> Evaluator
GET    /v1/evals/evaluators       list the org's evaluators          -> {data:[…]}
POST   /v1/evals/rubrics          create/upsert a rubric             -> ScoreConfig
GET    /v1/evals/rubrics          list the org's rubrics             -> {data:[…]}
POST   /v1/evals/scores           record a score event               -> ScoreView
GET    /v1/evals/scores           list score events (filters+limit)  -> {data:[…]}
GET    /v1/evals/traces           list traces (filters+limit)        -> {data:[…]}
POST   /v1/evals/runs             run a dataset through model+judge   -> runSummary
GET    /v1/evals/runs             list run records (datasetName)     -> {data:[…]}
GET    /v1/evals/metrics          the org's AI overview board        -> board

Order 145: binds /v1/evals/* BEFORE the AI subsystem's /v1/* catch-all (150), the same slot product uses. serve.go auto-registers GET /v1/evals/health.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount registers the /v1/evals/* surface on app per HIP-0106.

func Shutdown

func Shutdown() error

Shutdown releases the eval stores. Idempotent.

Types

type Board

type Board struct {
	Scope   BoardScope   `json:"scope"`
	Range   BoardRange   `json:"range"`
	Totals  BoardTotals  `json:"totals"`
	Series  []BoardPoint `json:"series"`
	ByModel []ModelStat  `json:"byModel"`
	Other   *ModelStat   `json:"other,omitempty"`
	Latency LatencyStat  `json:"latency"`
}

Board is the full AI-overview dashboard payload.

type BoardPoint

type BoardPoint struct {
	T           string `json:"t"` // RFC3339 (UTC) bucket start
	Generations int64  `json:"generations"`
	CostCents   int64  `json:"costCents"`
	TotalTokens int64  `json:"totalTokens"`
	Errors      int64  `json:"errors"`
}

BoardPoint is one gap-filled time bucket for the volume/cost/token/error series.

type BoardRange

type BoardRange struct {
	Range    string `json:"range"`    // echoed label (24h | 7d | 30d | custom)
	Start    string `json:"start"`    // RFC3339 (UTC)
	End      string `json:"end"`      // RFC3339 (UTC)
	Interval string `json:"interval"` // hour | day
}

type BoardScope

type BoardScope struct {
	Org     string `json:"org"` // "" when AllOrgs
	Project string `json:"project"`
	AllOrgs bool   `json:"allOrgs"`
}

type BoardTotals

type BoardTotals struct {
	Generations      int64   `json:"generations"`
	PromptTokens     int64   `json:"promptTokens"`
	CompletionTokens int64   `json:"completionTokens"`
	TotalTokens      int64   `json:"totalTokens"`
	CostCents        int64   `json:"costCents"`
	Errors           int64   `json:"errors"`
	SuccessRate      float64 `json:"successRate"` // 0..1
	Models           int64   `json:"models"`
	Users            int64   `json:"users"`
}

type Dataset

type Dataset struct {
	ID          string
	Org         string
	Name        string
	Description string
	Metadata    string // opaque JSON object, stored verbatim (bounded at the edge)
	CreatedAt   int64
	UpdatedAt   int64
}

Dataset is an org-scoped named collection of eval items. (org,name) is unique.

type DatasetItem

type DatasetItem struct {
	ID        string
	Org       string
	Dataset   string
	Input     string // opaque JSON, verbatim
	Expected  string // opaque JSON, verbatim ("expectedOutput" on the wire)
	Metadata  string // opaque JSON, verbatim
	Status    string // ACTIVE | ARCHIVED
	CreatedAt int64
	UpdatedAt int64
}

DatasetItem is one input/expected pair inside a dataset. Items are addressed by their own id; (org,dataset,id) scopes every lookup. Status ACTIVE|ARCHIVED mirrors the observation item lifecycle (a run consumes only ACTIVE items).

type DatasetRun

type DatasetRun struct {
	ID         string
	Org        string
	Dataset    string
	Name       string
	Model      string
	JudgeModel string
	Items      int
	Scored     int
	AvgScore   float64
	CreatedAt  int64
	UpdatedAt  int64
}

DatasetRun is the DEFINITION of a run (its metadata): which dataset+model, the run name, and rollup counters. The per-item scores/traces are telemetry (datastore); this row is the durable, listable run record. (org,dataset,name) is unique so a run name is stable per dataset.

type EvalRunner

type EvalRunner interface {
	// Complete runs one item's input through the model-under-test and returns its
	// raw text output.
	Complete(ctx context.Context, authz, model string, input any) (string, error)

	// Judge scores one item's output against the rubric using the judge model,
	// returning a numeric score in [0,1] and a one-line reasoning. It never
	// invents a score: an unparseable judge reply is an error the caller records
	// as the item's failure, not a fabricated 0 or 1.
	Judge(ctx context.Context, authz string, judge judgeSpec, input, expected any, output string) (float64, string, error)
}

EvalRunner is the PLUGGABLE execution seam (P3): the two independent steps of an evaluation, kept orthogonal so a DigitalOcean-backed runner and the in-process gateway runner both satisfy the same contract. The store, API and FE stay native and runner-agnostic regardless of which runner is wired.

The two steps are deliberately separate — Complete (produce the model-under-test output) and Judge (score that output against a rubric) — because DO's Agent Evaluations fuse run+judge into one async job with a FIXED (OpenAI) judge, whereas the gateway path needs them independent and needs the judge to be ANY model the caller can reach. A future DO adapter implements the same two methods (internally mapping a batch → evaluation_datasets upload → test_cases → evaluation_runs → poll → per-item results); nothing else changes.

Both methods take the caller's own Authorization bearer (authz): the run executes with the CALLER's identity and model entitlements — no privilege escalation, no service identity, the gateway authenticates it exactly as a direct call would.

type Evaluator

type Evaluator struct {
	ID        string
	Org       string
	Name      string
	Model     string
	Criteria  string
	ScoreName string // the score name this evaluator emits (defaults to Name)
	CreatedAt int64
	UpdatedAt int64
}

Evaluator is an org-scoped judge definition: a model + rubric that scores a run's items. (org,name) is unique. It carries NO secret — the model key is the caller's own bearer at run time, never stored here.

type LatencyStat

type LatencyStat struct {
	Available bool     `json:"available"`
	P50Ms     *float64 `json:"p50Ms"`
	P95Ms     *float64 `json:"p95Ms"`
	P99Ms     *float64 `json:"p99Ms"`
}

LatencyStat is the board's overall latency. Available=false with nil percentiles is the honest "no GenAI spans" state.

type MetricsFilter

type MetricsFilter struct {
	Org      string
	Project  string
	AllOrgs  bool
	Since    time.Time
	Until    time.Time
	Interval string
	TopN     int
}

MetricsFilter is the resolved, already-authorized dashboard query. Org is the validated tenant; AllOrgs (SuperAdmin) drops the org predicate; the window [Since, Until) is closed; Interval is the server-chosen bucket ("hour" | "day"); TopN bounds the by-model table (the rest fold into "other").

Project is the org SUB-SCOPE (server-minted, "" for the org's default project == whole-org view). When non-empty it ANDs a project predicate on the ledger and the latency spans, so the board narrows to that project WITHIN the org — org stays the hard tenant boundary regardless.

type ModelStat

type ModelStat struct {
	Model            string   `json:"model"`
	Provider         string   `json:"provider"`
	Requests         int64    `json:"requests"`
	PromptTokens     int64    `json:"promptTokens"`
	CompletionTokens int64    `json:"completionTokens"`
	TotalTokens      int64    `json:"totalTokens"`
	CostCents        int64    `json:"costCents"`
	Errors           int64    `json:"errors"`
	ErrorRate        float64  `json:"errorRate"` // 0..1
	CostPct          float64  `json:"costPct"`   // share of total spend, 0..100
	P50Ms            *float64 `json:"p50Ms"`
	P95Ms            *float64 `json:"p95Ms"`
	P99Ms            *float64 `json:"p99Ms"`
	ModelCount       int      `json:"modelCount,omitempty"` // >0 only on the "other" fold
}

ModelStat is one row of the model-usage table (or the folded "other" bucket). The latency percentiles are pointers so "no latency data for this model" is a null the console renders as "—", never a fabricated 0.

type ScoreConfig

type ScoreConfig struct {
	ID         string
	Org        string
	Name       string
	DataType   string   // NUMERIC | CATEGORICAL | BOOLEAN
	MinValue   *float64 // NUMERIC lower bound (nil = unbounded)
	MaxValue   *float64 // NUMERIC upper bound (nil = unbounded)
	Categories []string // CATEGORICAL allowed labels
	CreatedAt  int64
	UpdatedAt  int64
}

ScoreConfig is an org-scoped definition of a score's shape: NUMERIC (with optional min/max), CATEGORICAL (with an allowed value set), or BOOLEAN. Scores written for this name are validated against it (Red: score integrity).

type ScoreEvent

type ScoreEvent struct {
	ID          string
	Org         string
	Name        string
	TraceID     string
	RunName     string
	Dataset     string
	ItemID      string
	DataType    string
	Value       float64
	StringValue string
	Comment     string
	Timestamp   time.Time
}

ScoreEvent is one recorded score (append-only). Value is the numeric score; StringValue is the categorical/boolean label (empty for pure numeric). DataType is NUMERIC|CATEGORICAL|BOOLEAN. TraceID links it to its trace/run.

type ScoreFilter

type ScoreFilter struct {
	Org     string
	Name    string
	RunName string
	TraceID string
	Limit   int
}

ScoreFilter bounds a scores read. Org is MANDATORY (the caller passes the authoritative org); the others narrow within the org. Limit is always applied.

type Store

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

Store is the eval metastore over one SQLite file — the deployment's own "evals". Tenancy is the org column; MaxOpenConns(1) serializes writes against the file lock (same discipline as prompts/projects).

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) CountItems

func (s *Store) CountItems(ctx context.Context, org, dataset string) (int, error)

CountItems returns the number of items in (org,dataset) via a COUNT(*), so the dataset-detail view never has to load item bodies just to size the collection (Red LOW: loading up to maxListLimit full rows to len() them was a ~96MB amplification on a large dataset).

func (*Store) DeleteDataset

func (s *Store) DeleteDataset(ctx context.Context, org, name string) (bool, error)

DeleteDataset removes a dataset and its items in one transaction. Reports whether the dataset existed.

func (*Store) GetDataset

func (s *Store) GetDataset(ctx context.Context, org, name string) (Dataset, error)

func (*Store) GetEvaluator

func (s *Store) GetEvaluator(ctx context.Context, org, name string) (Evaluator, error)

func (*Store) GetItem

func (s *Store) GetItem(ctx context.Context, org, id string) (DatasetItem, error)

func (*Store) GetScoreConfig

func (s *Store) GetScoreConfig(ctx context.Context, org, name string) (ScoreConfig, error)

func (*Store) ListDatasets

func (s *Store) ListDatasets(ctx context.Context, org string, limit int) ([]Dataset, error)

func (*Store) ListEvaluators

func (s *Store) ListEvaluators(ctx context.Context, org string, limit int) ([]Evaluator, error)

func (*Store) ListItems

func (s *Store) ListItems(ctx context.Context, org, dataset string, activeOnly bool, limit int) ([]DatasetItem, error)

ListItems returns items for (org,dataset), newest first, bounded by limit. When activeOnly is set, ARCHIVED items are excluded (the run path uses this).

func (*Store) ListRuns

func (s *Store) ListRuns(ctx context.Context, org, dataset string, limit int) ([]DatasetRun, error)

func (*Store) ListScoreConfigs

func (s *Store) ListScoreConfigs(ctx context.Context, org string, limit int) ([]ScoreConfig, error)

func (*Store) PutItem

func (s *Store) PutItem(ctx context.Context, it DatasetItem) (DatasetItem, error)

PutItem inserts a new item or updates an existing one by (org,id). The dataset MUST already exist for this org (enforced by the caller via GetDataset); the item is bound to that dataset. Cross-org item ids can never collide because the WHERE always carries org.

func (*Store) UpsertDataset

func (s *Store) UpsertDataset(ctx context.Context, d Dataset) (Dataset, error)

UpsertDataset creates a dataset, or updates description/metadata when (org,name) already exists. Idempotent create keeps the FE's "ensure dataset" call safe. Returns the resulting row.

func (*Store) UpsertEvaluator

func (s *Store) UpsertEvaluator(ctx context.Context, e Evaluator) (Evaluator, error)

func (*Store) UpsertRun

func (s *Store) UpsertRun(ctx context.Context, r DatasetRun) (DatasetRun, error)

UpsertRun records or updates a run's metadata by (org,dataset,name). The run row is the durable, listable record; its per-item scores live in telemetry.

func (*Store) UpsertScoreConfig

func (s *Store) UpsertScoreConfig(ctx context.Context, c ScoreConfig) (ScoreConfig, error)

type Telemetry

type Telemetry interface {
	RecordTrace(ctx context.Context, t Trace) error
	RecordScore(ctx context.Context, sc ScoreEvent) error
	ListScores(ctx context.Context, f ScoreFilter) ([]ScoreEvent, error)
	ListTraces(ctx context.Context, f TraceFilter) ([]Trace, error)
	Metrics(ctx context.Context, f MetricsFilter) (Board, error)
	Close() error
}

Telemetry is the append-only event store for eval traces + scores. Every method takes an authoritative org (via the value's Org field or the filter); no method can read across orgs.

Production LLM generations ("observations") are NOT read here: the observation of record is the o11y gen_ai span plane (/v1/o11y/observations), not a second projection of hanzo.cloud_usage. cloud_usage stays the metering warehouse (read by /v1/usage + billing), never an obs store. eval owns only its two eval-specific tables (eval_traces, eval_scores).

type Trace

type Trace struct {
	ID         string
	Org        string
	ProjectID  string
	Name       string
	Dataset    string
	ItemID     string
	RunName    string
	SessionID  string
	APIKeyHash string
	Model      string
	Input      string
	Output     string
	StartTime  time.Time
	EndTime    time.Time
	Timestamp  time.Time
}

Trace is one model-under-test invocation recorded during a run. Input/Output are opaque JSON/text; the run/dataset/item fields carry the linkage.

Attribution (HIP-0106): a trace records WHO/WHERE/WHEN alongside WHAT, so the eval trace list carries the same observability dimensions the production gen_ai span plane does:

  • ProjectID — the org SUB-SCOPE (principal.Project); "" / "default" is the org's default project. Org stays the tenant-isolation key; project narrows.
  • SessionID — groups the traces of one logical session; an eval run stamps its RunName so a run's item-traces read back as one session.
  • APIKeyHash — a NON-reversible ref (SHA-256 hex) of the caller credential the run drove the model with. Never the plaintext key: the ref correlates a trace to a key without the store ever holding a secret.
  • StartTime/EndTime — the model-under-test call window; EndTime-StartTime is the trace latency the view surfaces (LatencyMs).

type TraceFilter

type TraceFilter struct {
	Org       string
	ProjectID string
	SessionID string
	RunName   string
	Dataset   string
	Limit     int
}

TraceFilter bounds a traces read. Org is MANDATORY; ProjectID/SessionID/RunName/ Dataset narrow WITHIN the org. ProjectID is the caller's server-minted project (principal.Project), so a project-scoped caller sees only its project's traces.

Jump to

Keyboard shortcuts

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