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:
- 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).
- 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.
- 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.
- 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
- Variables
- type Capabilities
- type Catalog
- func (c *Catalog) Close() error
- func (c *Catalog) Loaded() Loaded
- func (c *Catalog) Lookup(provider, model string) (Entry, bool)
- func (c *Catalog) Models(provider string) []Entry
- func (c *Catalog) Providers() []string
- func (c *Catalog) Refresh(ctx context.Context) (RefreshResult, error)
- func (c *Catalog) StatePath() string
- type CatwalkFetcher
- type Cost
- type Entry
- type Fetcher
- type HTTPFetcher
- type Limit
- type LoadOptions
- type Loaded
- type RefreshResult
- type Snapshot
- type Source
Constants ¶
const DefaultBaseURL = "https://catwalk.charm.land"
DefaultBaseURL is the canonical catwalk catalog host. The full URL fetched is BaseURL + "/v2/providers".
const DefaultHTTPTimeout = 15 * time.Second
DefaultHTTPTimeout caps the HTTP request when no client is injected.
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 ¶
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.
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 ¶
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 ¶
Loaded reports the in-memory snapshot's provenance, age, and size. Useful for `hygge catalog list` and diagnostic logging.
func (*Catalog) Lookup ¶
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 ¶
Models returns every entry for the given provider, sorted by id. Returns an empty slice when no provider matches.
func (*Catalog) Providers ¶
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.
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 ¶
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 ¶
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.
type Limit ¶
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" )