freemodels

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package freemodels publishes the cluster-wide opencode free-tier model catalog as a ConfigMap, so workspace pods can render their relay agent-config.json before opencode boots — eliminating the in-pod opencode-restart cycle that the legacy relay-injector goroutine imposed.

Why this is cluster-wide and not per-workspace:

The free model list comes from opencode's static catalog (proxied through models.dev). The filter applied is:

  • providerID == "opencode"
  • cost.input == 0

None of these vary per workspace — every pod that uses the free tier gets the same list. The pre-2026-06-23 implementation re-fetched it once per pod by spinning up opencode with a placeholder config, querying its /provider endpoint, then killing and restarting opencode with the real config. That ~6-8s cost was paid on every cold start and every resume. With this package, the controller fetches the list once per refreshInterval and publishes it as a ConfigMap; pods read the file directly during their bootstrap init container.

Failure semantics:

  • Initial fetch failure at controller startup: log, retry on the normal refresh interval. Workspaces created before the first successful fetch fall back to the legacy in-pod relay injector (it observes the missing/empty ConfigMap and runs unchanged).
  • Periodic refresh failure: keep the existing ConfigMap; new pods read the stale-but-valid catalog. The catalog changes rarely (new model added every few weeks), so a few hours of staleness is harmless.
  • models.dev outage: same as above. The fetched list is durable.

Concurrency: the refresher Runnable owns all writes to the ConfigMap. Reconcilers do not touch it; they only mount it into pod specs.

Index

Constants

View Source
const ConfigMapKey = "models.json"

ConfigMapKey is the key inside the ConfigMap whose value is the JSON-encoded free model list.

View Source
const ConfigMapName = "llmsafespaces-free-models"

ConfigMapName is the name of the ConfigMap published into the controller's namespace. Workspace pods reference it by name when the chart renders their pod-spec ConfigMap mount.

View Source
const ModelsDevAPIURL = "https://models.dev/api.json"

ModelsDevAPIURL is the public catalog opencode itself proxies to. Pulling from this URL avoids a chicken-and-egg dependency on having a workspace pod running just to discover models.

Variables

This section is empty.

Functions

func SyncConfigMap

func SyncConfigMap(ctx context.Context, c client.Client, namespace string, catalog Catalog) error

SyncConfigMap creates or updates the free-models ConfigMap with the supplied catalog. The CM has no ownerReference because it is controller-managed (same lifecycle pattern as the relay-router peers CM in controller/internal/relay/router_configmap.go).

Wire format: the `data["models.json"]` payload contains ONLY the model array (`{"models":[...]}`). The fetch timestamp and source URL — which would otherwise change on every refresh and defeat the no-op fast path — live in annotations (freemodels.llmsafespaces.dev/{fetched-at, source}).

No-op fast path: when the existing CM already contains an identical `data["models.json"]` payload AND no ownerReferences need stripping, returns nil without calling Update. The annotations are still refreshed via Update only when something else needs updating; we don't bump RV just to record a new timestamp. This is the common case during periodic refreshes when models.dev hasn't changed.

Phase B note: the no-op fast path matters for downstream pods that mount this CM as a projected volume — every Update can trigger a kubelet volume refresh and spurious agent-config rebuilds. Keeping the CM byte-stable when the catalog is unchanged is the contract the no-op fast path delivers on.

Types

type Catalog

type Catalog struct {
	Models    []Model   `json:"models"`
	FetchedAt time.Time `json:"fetched_at"`
	Source    string    `json:"source"`
}

Catalog is the wire-format envelope written into the ConfigMap. FetchedAt is included so operators can observe how stale the catalog is. agentd does not need this field; it is purely diagnostic.

type Fetcher

type Fetcher struct {
	// URL is the upstream catalog endpoint. Defaults to
	// ModelsDevAPIURL when empty.
	URL string
	// HTTPClient overrides the default client. Tests inject one with
	// a tighter timeout; production uses a default 30s client.
	HTTPClient *http.Client
}

Fetcher fetches the free model list from a configured upstream URL. Defaults to models.dev. Tests inject a fake server URL.

func (*Fetcher) Fetch

func (f *Fetcher) Fetch(ctx context.Context) ([]Model, error)

Fetch returns the free-tier opencode model list. A model is "free" iff its cost.input is exactly zero. The result is sorted by model ID for stable ConfigMap diffs (so Update() is a no-op when the catalog is unchanged).

Returns an empty slice (not an error) when the upstream response contains no opencode entry or no free models. Callers can decide whether to treat that as a hard failure; the controller-side Runnable treats it as "skip this refresh, keep existing CM".

type Model

type Model struct {
	ID           string `json:"id"`
	Name         string `json:"name"`
	ContextLimit int    `json:"context_limit"`
	OutputLimit  int    `json:"output_limit"`
}

Model is the minimal model info needed to render the relay provider entry in agent-config.json. Field names match the JSON consumed by agentd's materialize subcommand (see cmd/workspace-agentd/secrets.go applyRelayConfig). Stable wire format — changes here require a matching change in the agentd reader.

type Refresher

type Refresher struct {
	Client    client.Client
	Namespace string
	// Interval governs how often the catalog is re-fetched. Production
	// should use 6h; the catalog changes ~weekly. Tests inject a much
	// shorter interval and rely on a fake server URL.
	Interval time.Duration
	// Fetcher is the catalog source. Tests inject one with a fake URL.
	Fetcher *Fetcher
}

Refresher is a controller-runtime Runnable that periodically fetches the free model catalog and publishes it as a ConfigMap. Wire it via mgr.Add(refresher) in main.go.

First fetch runs at Start; subsequent fetches every Interval. A fetch failure does NOT delete the existing ConfigMap — stale-but- valid is strictly better than absent (workspace pods would fall back to the legacy in-pod relay injector path).

func (*Refresher) NeedLeaderElection

func (r *Refresher) NeedLeaderElection() bool

NeedLeaderElection ensures only one controller replica refreshes the catalog. Without this, every replica would fetch and write independently — wasteful and would generate spurious ResourceVersion churn on the CM.

func (*Refresher) Start

func (r *Refresher) Start(ctx context.Context) error

Start implements manager.Runnable. Returns when ctx is canceled. Errors from individual fetches are logged but do not propagate — returning a non-nil error here would tear down the manager, which is the wrong response to a transient upstream outage.

Jump to

Keyboard shortcuts

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