catalog

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package catalog is hygge's central source of truth for model metadata: pricing, capabilities, context-window limits, and modalities.

Sources

Data is sourced from the catwalk catalog service with three layers of fallback:

  1. A disk-cached snapshot at $XDG_STATE_HOME/hygge/catalog.json, refreshed on demand via Catalog.Refresh (which is wired to the `hygge catalog refresh` CLI command).
  2. An embedded snapshot derived from the catwalk module's built-in provider configs. This is the bedrock fallback: hygge always has at least this catalog available even when offline and no disk cache exists.
  3. An optional background refresh kicked off at Load time when the disk cache is missing or older than LoadOptions.MaxStaleness. The refresh runs in a goroutine and NEVER blocks startup.
  4. An optional periodic ticker, started when LoadOptions.RefreshInterval is positive, that calls Catalog.Refresh at that cadence. This lets a long-lived hygge process pick up upstream catalog changes without a restart. The ticker exits promptly on Catalog.Close.

Schema

Parsed from the catwalk /v2/providers JSON array. The fields hygge depends on per model are:

{
  "id":                       string,
  "name":                     string,
  "cost_per_1m_in":           float,
  "cost_per_1m_out":          float,
  "cost_per_1m_in_cached":    float,
  "cost_per_1m_out_cached":   float,
  "context_window":           int,
  "default_max_tokens":       int,
  "can_reason":               bool,
  "reasoning_levels":         []string,
  "default_reasoning_effort": string,
  "supports_attachments":     bool
}

Unknown fields inside model entries are silently ignored.

ETag caching

The CatwalkFetcher uses HTTP ETag conditional requests. The ETag received from the server is stored in the on-disk snapshot so that subsequent calls can send If-None-Match, saving bandwidth when the catalog has not changed (server replies 304). An old-format v1 disk snapshot (from the prior models.dev integration) is detected by its version number and silently discarded so the catwalk snapshot is fetched fresh.

Concurrency

Catalog is safe for concurrent reads. Refresh holds an internal write lock for the duration of the network fetch and disk write. The periodic-refresh ticker runs in its own goroutine; concurrent calls to Refresh (from the ticker, from backgroundRefresh, and from `hygge catalog refresh`) are serialised by the internal refreshing mutex so only one network fetch is in flight at a time.

Boundaries

This package depends on charm.land/catwalk and the standard library. It must not import internal/agent, internal/store, internal/provider, or internal/cost. Both internal/cost and internal/provider/* consume a *Catalog handed to them by the cmd/hygge/cli bootstrap; this package never reaches up.

Index

Constants

View Source
const DefaultBaseURL = "https://catwalk.charm.land"

DefaultBaseURL is the canonical catwalk catalog host. The full URL fetched is BaseURL + "/v2/providers".

View Source
const DefaultHTTPTimeout = 15 * time.Second

DefaultHTTPTimeout caps the HTTP request when no client is injected.

View Source
const DefaultMaxStaleness = 7 * 24 * time.Hour

DefaultMaxStaleness is the freshness window used when LoadOptions does not set one explicitly. After this much time has passed since the disk snapshot's FetchedAt, Load schedules a background refresh.

Variables

View Source
var ErrIncompatibleSnapshot = errors.New("catalog: incompatible disk snapshot")

ErrIncompatibleSnapshot marks an expected on-disk cache miss caused by an older cache schema. Callers should fall back without surfacing it as corruption.

View Source
var ErrNotModified = errors.New("catalog: not modified (304)")

ErrNotModified is returned by CatwalkFetcher.FetchWithETag when the server replies 304 Not Modified.

Functions

This section is empty.

Types

type Capabilities

type Capabilities struct {
	// Reasoning indicates the model produces an explicit thinking /
	// reasoning trace (OpenAI's o-series, Anthropic with the thinking
	// block, etc.).
	Reasoning bool

	// ToolCalling indicates the model supports function/tool-use
	// blocks in the assistant turn.
	ToolCalling bool

	// Attachment indicates the model accepts file attachments.
	// Distinct from InputImages: a model can accept image input
	// inline (in a chat block) without supporting attachment APIs.
	Attachment bool

	// InputText indicates the model accepts text input.  Set by
	// inspecting modalities.input for the "text" element.
	InputText bool

	// InputImages indicates the model accepts image input.
	InputImages bool

	// OutputText indicates the model emits text output.
	OutputText bool

	// OutputImages indicates the model emits image output (rare).
	OutputImages bool
}

Capabilities is the set of boolean feature flags upstream models.dev advertises. Zero value = "not advertised" — callers should treat that as "unknown / probably false".

type Catalog

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

Catalog is the public handle. Construct via Load.

func Load

func Load(opts LoadOptions) (*Catalog, error)

Load constructs a Catalog. Always returns a usable handle: even when the disk cache is missing AND the background refresh fails, the embedded snapshot is loaded so Catalog.Lookup still answers.

Errors are returned only for catastrophic situations — for example, the embedded snapshot itself failing to parse, which would mean the binary was built wrong.

func (*Catalog) Close

func (c *Catalog) Close() error

Close stops the periodic-refresh ticker (if any) and waits for its goroutine to exit. Idempotent. Returns nil.

Callers that hold a long-lived Catalog (e.g. the CLI runtime) should defer Close so the ticker goroutine is not leaked after shutdown.

func (*Catalog) Loaded

func (c *Catalog) Loaded() Loaded

Loaded reports the in-memory snapshot's provenance, age, and size. Useful for `hygge catalog list` and diagnostic logging.

func (*Catalog) Lookup

func (c *Catalog) Lookup(provider, model string) (Entry, bool)

Lookup returns the catalog entry for (provider, model). Both arguments are matched against the canonical id used by models.dev. Returns ok=false when not found.

The lookup is case-insensitive on provider and on model, since upstream provider ids are sometimes spelled differently across hygge's surfaces (e.g. user typing "Anthropic" in a config field) but models.dev canonicalises to lowercase.

func (*Catalog) Models

func (c *Catalog) Models(provider string) []Entry

Models returns every entry for the given provider, sorted by id. Returns an empty slice when no provider matches.

func (*Catalog) Providers

func (c *Catalog) Providers() []string

Providers returns the sorted list of provider ids in the current snapshot.

func (*Catalog) Refresh

func (c *Catalog) Refresh(ctx context.Context) (RefreshResult, error)

Refresh fetches a fresh snapshot from the source and persists it to disk. Blocking. Single-flight: concurrent Refresh calls collapse to one underlying fetch.

When the source is a CatwalkFetcher the current snapshot's ETag is forwarded as If-None-Match. If the server replies 304 Not Modified, Refresh returns successfully with the existing snapshot counts and a zero PreviousAge — the in-memory and on-disk state are unchanged.

On success (new data) the in-memory snapshot is replaced and the disk cache is rewritten atomically. On failure the previous snapshot is preserved and the error is returned.

func (*Catalog) StatePath

func (c *Catalog) StatePath() string

StatePath returns the absolute path of the on-disk snapshot file, or the empty string when no state directory was resolved.

type CatwalkFetcher

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

CatwalkFetcher is the production Fetcher that talks to the catwalk catalog service. It uses ETag-based conditional requests to avoid transferring unchanged data.

func NewCatwalkFetcher

func NewCatwalkFetcher(client *http.Client, baseURL string) *CatwalkFetcher

NewCatwalkFetcher builds a CatwalkFetcher. client must be non-nil. baseURL is the catwalk server root (scheme + host, no trailing slash).

func (*CatwalkFetcher) Fetch

func (f *CatwalkFetcher) Fetch(ctx context.Context) (*Snapshot, error)

Fetch implements Fetcher. Returns a parsed Snapshot on success. Errors cover transport failures, non-2xx responses, body-read failures, and malformed JSON.

ETag from the server response is stored in the returned snapshot so the on-disk cache can forward it on the next call. Call CatwalkFetcher.FetchWithETag directly when you have an existing tag.

func (*CatwalkFetcher) FetchWithETag

func (f *CatwalkFetcher) FetchWithETag(ctx context.Context, etag string) (*Snapshot, error)

FetchWithETag performs an ETag-conditional fetch. When etag is non-empty it is sent as If-None-Match. Returns (nil, ErrNotModified) when the server replies 304.

type Cost

type Cost struct {
	Input      float64
	Output     float64
	CacheRead  float64
	CacheWrite float64
}

Cost is the per-1-million-token pricing in USD. Same shape as internal/cost's Pricing but lives in this package to keep the boundary clean.

type Entry

type Entry struct {
	// Provider is the canonical catwalk provider id, e.g. "anthropic"
	// or "openai".  Lowercase, no spaces.
	Provider string

	// ID is the bare model id, e.g. "claude-sonnet-4-5".  For
	// providers that namespace their ids (OpenRouter uses
	// "<vendor>/<model>"), the id retains its full namespace form
	// as the catalog publishes it.
	ID string

	// Name is the human-readable display name from the catalog, e.g.
	// "Claude Sonnet 4.5".  May be empty when the upstream
	// catalog omits it.
	Name string

	// Capabilities collects the boolean feature flags advertised by
	// the upstream catalog.
	Capabilities Capabilities

	// Limit reports the context-window and per-call output caps.
	Limit Limit

	// Cost is the per-million-token pricing.  Zero values mean "this
	// model does not charge for that token class" (e.g. no caching).
	Cost Cost

	// ReleaseDate is a free-form date string ("2025-09-29", etc.).
	// May be empty.  Populated only by the legacy models.dev path.
	ReleaseDate string

	// ReasoningLevels is the set of reasoning effort levels the model
	// supports, e.g. ["low", "medium", "high"].  Empty when the model
	// does not advertise explicit levels.
	ReasoningLevels []string `json:"reasoning_levels,omitempty"`

	// DefaultReasoningEffort is the effort level the provider recommends
	// when the caller does not specify one, e.g. "high".  May be empty.
	DefaultReasoningEffort string `json:"default_reasoning_effort,omitempty"`

	// Source identifies which layer produced this entry.  Set when the
	// Catalog hands an Entry to a caller; not persisted in JSON.
	Source Source `json:"-"`
}

Entry is one model in the catalog: a flat denormalised view across the provider, id, capability flags, limits, and pricing.

type Fetcher

type Fetcher interface {
	// Fetch returns a populated snapshot, or an error.  The snapshot's
	// FetchedAt field will be overwritten by the Catalog using the
	// current clock — implementations need not set it.
	Fetch(ctx context.Context) (*Snapshot, error)
}

Fetcher is the source interface the Catalog uses to obtain a fresh snapshot. Production wires this to the real models.dev fetcher; tests inject a stub.

type HTTPFetcher

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

HTTPFetcher is the legacy Fetcher that hits a models.dev-style HTTP endpoint. It is still used by tests that seed a fake server with models.dev JSON. Production uses CatwalkFetcher.

func NewHTTPFetcher

func NewHTTPFetcher(client *http.Client, baseURL string) *HTTPFetcher

NewHTTPFetcher builds a legacy HTTPFetcher.

func (*HTTPFetcher) Fetch

func (h *HTTPFetcher) Fetch(ctx context.Context) (*Snapshot, error)

Fetch implements Fetcher via the legacy models.dev /api.json endpoint.

type Limit

type Limit struct {
	ContextWindow int64
	MaxOutput     int64
}

Limit is the context-window and per-call output token cap. Zero means the upstream catalog did not publish a value.

type LoadOptions

type LoadOptions struct {
	// StateDir is the directory the disk snapshot lives in.  The
	// actual file is <StateDir>/catalog.json.  Empty falls back to
	// $XDG_STATE_HOME/hygge (or ~/.local/state/hygge).
	StateDir string

	// Source is an injectable fetcher for tests.  Nil uses the real
	// catwalk fetcher.
	Source Fetcher

	// HTTPClient is used for live fetches when Source is nil.  Nil
	// defaults to an [http.Client] with [DefaultHTTPTimeout].
	HTTPClient *http.Client

	// BaseURL overrides the catwalk host when Source is nil.
	// Empty falls back to [DefaultBaseURL].  Tests point this at an
	// httptest server.
	BaseURL string

	// Now is an injectable clock for tests.  Nil uses [time.Now].
	Now func() time.Time

	// BackgroundRefresh enables the on-Load goroutine that refreshes
	// stale snapshots in the background.  Defaults to true; tests
	// typically set it to false for determinism.
	BackgroundRefresh *bool

	// MaxStaleness is the age beyond which an on-disk snapshot is
	// considered stale and triggers the background refresh.  Zero
	// defaults to [DefaultMaxStaleness].
	MaxStaleness time.Duration

	// RefreshInterval, when positive, starts a background ticker that
	// calls [Catalog.Refresh] at that cadence.  Zero (the default)
	// means no periodic refresh — the one-shot background refresh
	// still fires on startup when BackgroundRefresh is enabled.
	//
	// The ticker exits promptly when [Catalog.Close] is called.
	// Configured via [catalog] refresh_interval in config.toml.
	RefreshInterval time.Duration
}

LoadOptions configures Load. Zero value is valid and yields a production-defaults Catalog.

type Loaded

type Loaded struct {
	Source      Source
	FetchedAt   time.Time
	Age         time.Duration
	Providers   int
	Models      int
	StateDir    string
	SnapshotURL string
}

Loaded summarises the in-memory snapshot's provenance and age. Returned by Catalog.Loaded for diagnostic surfaces.

type RefreshResult

type RefreshResult struct {
	// Providers is the number of distinct provider ids in the new
	// snapshot.
	Providers int
	// Models is the total model count across all providers.
	Models int
	// FetchedAt is when the new snapshot was produced.
	FetchedAt time.Time
	// PreviousAge is the age of the snapshot the new fetch replaced,
	// or zero when no prior snapshot existed.
	PreviousAge time.Duration
}

RefreshResult is returned by Catalog.Refresh.

type Snapshot

type Snapshot struct {
	FetchedAt time.Time                   `json:"fetched_at"`
	ETag      string                      `json:"etag,omitempty"`
	Providers map[string]map[string]Entry `json:"providers"`
}

Snapshot is the parsed, normalised in-memory and on-disk representation of the catalog. Providers maps provider id to model id to Entry.

FetchedAt is the time the snapshot was produced (network fetch time for live data; zero for the embedded snapshot).

ETag is the HTTP ETag received from the catwalk server, forwarded as If-None-Match on the next conditional refresh. Empty for the embedded snapshot and for disk snapshots written before ETag support was added.

type Source

type Source string

Source identifies which layer in the resolution cascade produced the current in-memory snapshot. Surfaced via Catalog.Loaded for diagnostics; not used for correctness.

const (
	// SourceEmbedded means the snapshot came from the catwalk module's
	// built-in provider configs, loaded at startup.
	SourceEmbedded Source = "embedded"
	// SourceDisk means the snapshot was read from the on-disk cache
	// at $XDG_STATE_HOME/hygge/catalog.json.
	SourceDisk Source = "disk"
	// SourceNetwork means the snapshot was just fetched from
	// models.dev.
	SourceNetwork Source = "network"
)

Jump to

Keyboard shortcuts

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