Help us shape the future of Go! Take the 2026 Go Developer Survey

llmkit

package
v0.3.0 Latest Latest
Warning

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

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

README

llmkit

llmkit is an optional routing module for choosing an LLM model/account from host-provided task metadata. It keeps routing decisions deterministic and auditable while leaving provider clients, API keys, prompts, responses, and business workflow ownership in the host application.

Configuration Home

LLMKIT_HOME points to the directory where a host keeps llmkit runtime configuration and audit files.

  • Development mode may fall back to .llmkit under the current working directory.
  • Production mode requires LLMKIT_HOME; fail startup if it is unset.
  • Relative values are resolved from the host process working directory.

Example:

export LLMKIT_HOME=/srv/my-agent/.llmkit

Load the host configuration explicitly:

config, err := llmkit.LoadConfigFromEnv(os.Getenv)
if err != nil {
    return err
}

candidates := config.Candidates()
defaultProfile := config.DefaultTaskProfile()

LoadConfigFromEnv resolves LLMKIT_HOME in production mode and reads config.yaml. LoadConfig(home) is available when a host has already resolved the directory. The loader validates account/model references and rejects plaintext api_key; it does not construct provider clients or read secret values from api_key_env.

Example config.yaml:

accounts:
  - alias: local-dev
    provider: openai_compatible
    base_url: http://127.0.0.1:11434/v1
  - alias: cloud-prod
    provider: openai_compatible
    base_url: https://api.example.com/v1
    api_key_env: CLOUD_PROD_API_KEY

models:
  - alias: local-free
    model: qwen2.5:7b
    provider: openai_compatible
    account_alias: local-dev
    is_local: true
    capability_level: simple
    context_window_class: medium
    price_class: free
    latency_class: fast
    estimated_cents: 0
  - alias: cloud-advanced
    model: advanced-model
    provider: openai_compatible
    account_alias: cloud-prod
    capability_level: advanced
    context_window_class: long
    price_class: high
    latency_class: normal
    estimated_cents: 8

The host is responsible for failing closed when a configured api_key_env is missing. examples/host-api demonstrates that behavior: if config.yaml exists and references an unset secret environment variable, server startup fails instead of silently routing unauthenticated requests.

Routing Intent

The default policy applies hard filters first, then ranks eligible candidates by capability, price, locality, latency, recent reliability, and current provider health.

  • Reliability is minus the smoothed failure rate in percent, bounded to [-100, 0]. Observed outcomes are blended with a 5% prior worth 10 observations, so a model with no history scores -5, a long clean history beats no history, and ten straight failures read as a majority failure rate.
  • Latency uses observed average latency from model stats when present (at most 2s is fast, at most 10s is normal, slower is slow) and falls back to the configured latency_class.

Simple tasks should be local-first when the profile allows it. For example, low failure-cost classification, short rewriting, formatting, or simple JSON extraction can prefer a free local model with fast latency and low operational cost.

Hard or high failure-cost tasks should route to an advanced model. For example, tasks that need stronger reasoning, long context, tool use, strict JSON, or reviewer-visible correctness should be profiled with complexity: hard, failure_cost: high, or the relevant needs_* flags so local simple models are filtered out or outscored.

Hosts can set TaskProfile.MaxEstimatedCents to make cost a hard routing constraint. Candidates with a known ModelCapability.EstimatedCents above that limit are filtered out before scoring. ApplyModelStats also feeds AvgEstimatedCents back into candidates, so observed cost can enforce future budget constraints. Project/account-level budget governance remains host-owned; see ../docs/llmkit-budget-governance.md.

Hosts provide TaskProfile; llmkit does not infer business risk from prompt text by itself. If the host does not provide a profile, DefaultTaskProfile uses a conservative medium/cloud-allowed baseline.

Audit Files

JSONLRecorder writes two append-only audit files under LLMKIT_HOME:

  • route-events.jsonl: sanitized routing decisions, effective task profile, selected aliases, provider name, score, score breakdown, compact candidate aliases, and full candidate-level score/filter explanations.
  • outcomes.jsonl: sanitized outcome metadata such as provider success, business outcome signal, error code, latency, token counts, and estimated cents.

Audit records intentionally avoid prompts, responses, headers, and API keys. The recorder also sanitizes key-like strings before writing JSONL.

Hosts can periodically rebuild model-stats.json from those audit files:

stats, err := llmkit.RefreshModelStats(llmkitHome)
if err != nil {
    return err
}
_ = stats

model-stats.json groups records by task_type, account_alias, model_alias, and provider. Each bucket includes route attempts, completed outcomes, pending outcomes, success/failure rates, average latency, average token counts, average estimated cents, and last seen time. Outcomes are matched to routes by route_id and attempt, so every fallback attempt is counted; average latency and cost only include outcomes that reported them. recent_outcomes and recent_failures weight each outcome by its age, halving every ModelStatsHalfLife (7 days); reliability scoring uses these, so a model that failed weeks ago but works now is not held back by its history. The file is derived data: keep route-events.jsonl and outcomes.jsonl as the append-only source of truth.

Hosts can also expose route-level observability by reading the same audit files:

records, err := llmkit.ReadRouteAudits(llmkitHome, llmkit.AuditFilter{
    TaskID: workflowID,
})
if err != nil {
    return err
}

ReadRouteAudits joins route traces with matching outcomes by route_id. It returns only the allowlisted audit fields recorded in JSONL; it does not read or reconstruct prompts, responses, headers, or API keys.

To make routing use this history, load the generated stats and pass them to the adapter:

modelStats, err := llmkit.LoadModelStats(llmkitHome)
if err != nil {
    return err
}

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates: config.Candidates(),
    Providers:  providers,
    ModelStats: modelStats,
})

The adapter applies stats after it receives the current TaskProfile, so history is task-type aware. Without ModelStats, routing behavior is unchanged.

Long-running hosts can provide fresh stats for every route decision:

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates: config.Candidates(),
    Providers:  providers,
    ModelStatsProvider: func(ctx context.Context) (*llmkit.ModelStats, error) {
        return llmkit.RefreshModelStats(llmkitHome)
    },
})

This keeps newly written outcomes useful without waiting for a process restart.

Provider Health

MemoryHealthStore tracks current provider/account/model runtime state:

  • in-flight calls and concurrency ceilings
  • quota exhaustion
  • failure streaks, counting only failures attributable to the provider; a cancelled, oversized, or over-budget request does not degrade it
  • degraded or unavailable providers
  • cooldown windows after repeated failures; each failure past the threshold, such as a failed recovery probe, doubles the cooldown up to HealthPolicy.MaxCooldownDuration (default 32x CooldownDuration)

Hosts can keep this in memory for a single process, or replace the interface with a durable/shared implementation later. The route policy treats quota exhaustion, active cooldown, unavailable providers, and full provider or account concurrency (including max_concurrency from config) as hard filters. Degraded providers remain eligible but receive a lower score, so they are used only when better options are unavailable. After a cooldown expires, an unavailable provider is half-open: exactly one probe call tests recovery while the others wait for its outcome.

Typical adapter wiring:

health := llmkit.NewMemoryHealthStore(llmkit.HealthPolicy{
    FailureCooldownThreshold: 3,
    CooldownDuration:         30 * time.Second,
})

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates:   config.Candidates(),
    Providers:    providers,
    HealthStore:  health,
    ModelStats:   modelStats,
    Recorder:     recorder,
})

The adapter applies HealthStore.Snapshot() before each routing decision. Because the snapshot can be stale under concurrency, it then calls Acquire, which checks the same limits atomically and takes the slot; a candidate that turns out busy is skipped without spending a fallback attempt. After the call the adapter records the outcome, or Releases the slot if the provider was never called. This keeps simple tasks local-first when local is healthy, while allowing fallback to cloud when the local provider is busy, quota-exhausted, or cooling down after failures.

MemoryHealthStore is intentionally a single-process implementation. The shared-store contract and future host replacement path are documented in ../docs/llmkit-healthstore-contract.md.

Fallback Policy

When a selected provider fails, the adapter removes that candidate and asks the policy to select the next best provider-backed candidate. Each attempt records a route trace with an incremented attempt number and, when enabled, a provider outcome.

Hosts can cap fallback attempts:

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates: config.Candidates(),
    Providers:  providers,
    FallbackPolicy: goagentadapter.FallbackPolicy{
        MaxAttempts: 2,
    },
})

MaxAttempts <= 0 preserves the default behavior: try all remaining eligible provider-backed candidates.

Reliability-sensitive runtimes should fail closed and allowlist only errors that are known to occur before dispatch:

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates: config.Candidates(),
    Providers:  providers,
    FallbackPolicy: goagentadapter.FallbackPolicy{
        FailClosed: true,
        RetryableErrorClasses: []llmkit.ErrorClass{
            llmkit.ErrorClassTransient,
            llmkit.ErrorClassTimeout,
        },
    },
})

A fail-closed client never falls back when the provider error is dispatched or its dispatch status is unknown. Setting Config.PreDispatch (a typed host claim boundary run before each provider call) implies FailClosed.

Hosts can also classify provider errors for audit and future typed fallback rules:

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates: config.Candidates(),
    Providers:  providers,
    ErrorClassifier: func(err error) llmkit.ErrorClass {
        return llmkit.ErrorClassTimeout
    },
})

Without ErrorClassifier, provider failures keep the existing error_code=provider_error outcome and fallback behavior. With a classifier, failed outcomes also record error_class, such as timeout or rate_limited.

The typed fallback contract is documented in ../docs/plans/2026-05-07-llmkit-host-contract-followups-design.md.

API Keys

Do not store plaintext API keys in llmkit config files or audit files. Configuration should reference secret material through environment variable names such as api_key_env: OPENAI_API_KEY, account aliases, or a host-owned secret store.

Recommended rules:

  • Store only alias, account_alias, provider metadata, capability labels, and api_key_env names in config.
  • Resolve the actual key in the host/provider client at request time.
  • Keep provider clients responsible for transport headers and credential loading.
  • Do not copy keys into TaskProfile, Candidate, RouteTrace, TaskOutcome, logs, or errors.

goagent Adapter

The adapter in adapters/goagent implements github.com/eruca/goagents/goagent/ports.LLMClient.

Typical usage:

recorder, err := llmkit.NewJSONLRecorder(llmkitHome)
if err != nil {
    return err
}

providers, err := goagentadapter.OpenAICompatibleProvidersFromConfig(*config, os.Getenv, nil)
if err != nil {
    return err
}

client := goagentadapter.NewClient(goagentadapter.Config{
    Candidates: config.Candidates(),
    Providers:  providers,
    ProfileProvider: func(ctx context.Context, req ports.ChatRequest) llmkit.TaskProfile {
        profile := config.DefaultTaskProfile()
        profile.Source = llmkit.ProfileSourceHost
        profile.TaskType = "answer"
        profile.Complexity = llmkit.ComplexityHard
        profile.FailureCost = llmkit.FailureCostHigh
        profile.NeedsReasoning = true
        return profile
    },
    RouteMetadataProvider: func(ctx context.Context, req ports.ChatRequest) goagentadapter.RouteMetadata {
        return goagentadapter.RouteMetadata{
            RouteID: routeID,
            TaskID:  taskID,
            Attempt: attempt,
        }
    },
    Recorder: recorder,
})

Import aliases are host-owned; one common pattern is:

import (
    goagentadapter "github.com/eruca/goagents/llmkit/adapters/goagent"
    "github.com/eruca/goagents/llmkit"
)

The provider map keys must match Candidate.Model.Alias. Missing provider-backed candidates are skipped before routing.

OpenAICompatibleProvidersFromConfig supports provider: openai, provider: openai_compatible, and local OpenAI-compatible servers. It uses account base_url, model model, and account api_key_env; if model is omitted, the model alias is used as the provider model id. Passing nil as the HTTP client uses the default client.

Its limits argument applies the same openaiapi.Limits to every provider it builds, and ports.ChatRequest.MaxOutputTokens reaches the selected provider unchanged. Zero values disable a bound; limits are never silently inferred from an application profile.

The adapter implements ports.StreamingLLMClient, so Agent.Stream receives model output as it is generated. ChatStream streams through providers that implement ports.StreamingLLMClient, such as openaiapi, and delivers any other provider's complete response as one delta. An attempt that has already delivered output never falls back to another candidate, so a consumer never sees two answers spliced together. With a pre-dispatch hook, streaming providers must implement ProviderStreamRequestDigester so the claim covers the exact streaming request body.

When a selected provider fails, the adapter removes that candidate and asks the policy to select the next best provider-backed candidate. Each attempted route is recorded with an incremented attempt value when a recorder is configured.

See examples/goagent-routing for a minimal host-style example that loads LLMKIT_HOME/config.yaml, builds OpenAI-compatible providers, wires the goagent adapter, runs one request, and writes route-events.jsonl plus outcomes.jsonl.

Host API Composition Example

examples/host-api shows a fuller host composition. It is not a new core module; it combines workflowkit, artifactkit, runkit, goagent, and llmkit behind HTTP endpoints.

Important behavior:

  • If LLMKIT_HOME/config.yaml is absent, the example uses deterministic static demo providers so tests and walkthroughs run without network access.
  • If config.yaml exists, host-api builds candidates and OpenAI-compatible providers from that file.
  • Startup validates and initializes model-stats.json. During runtime, host-api refreshes stats before each route decision through ModelStatsProvider, and refreshes again when serving /llmkit/models.
  • GET /workflows/{id}/llm-routes exposes route audit for a workflow.
  • GET /llmkit/models exposes configured models, current health snapshot, and model stats used for history-aware routing.

See ../docs/host-api-contract.md for the prose endpoint contract and ../examples/host-api/openapi.yaml for the machine-readable contract.

Testing

go test ./llmkit/...

The core llmkit package does not import goagent; only llmkit/adapters/goagent does. internal/boundary enforces this.

Documentation

Overview

Package llmkit defines contracts for routing LLM calls across accounts and models.

llmkit helps host applications choose a model and account for a task based on task profile, model capability, cost, latency, concurrency, and audit needs. It owns routing, account/model policy, and audit contracts, but it does not store API keys. Implementations should refer to keys through host-owned environment variables, secret stores, or aliases.

llmkit is an optional adapter/capability module in the goagents workspace. It is not part of github.com/eruca/goagents/goagent core, and goagent must not import it.

Index

Constants

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

ModelStatsHalfLife is the age at which an outcome counts half as much as a fresh one in RecentOutcomes and RecentFailures.

Variables

View Source
var ErrProviderBusy = errors.New("provider cannot take another call now")

ErrProviderBusy is wrapped by HealthStore.Acquire when a provider cannot take another call right now. It is not a provider failure: callers move on to another candidate and record no outcome.

Functions

func CountsTowardProviderHealth

func CountsTowardProviderHealth(class ErrorClass) bool

CountsTowardProviderHealth reports whether a failure says something about the provider. Failures the caller caused, such as cancelling the request or sending one that is too large or over budget, leave provider health alone. An unclassified failure counts, which keeps the conservative default.

func ProviderHealthKey

func ProviderHealthKey(accountAlias, modelAlias, provider string) string

func ResolveHome

func ResolveHome(cwd string, getenv func(string) string, mode HomeMode) (string, error)

ResolveHome resolves llmkit's configuration home without creating it.

func WriteModelStats

func WriteModelStats(home string, stats ModelStats) error

WriteModelStats writes model-stats.json under home with private permissions.

Types

type AccountConfig

type AccountConfig struct {
	Alias          string `yaml:"alias,omitempty" json:"alias,omitempty"`
	Provider       string `yaml:"provider,omitempty" json:"provider,omitempty"`
	BaseURL        string `yaml:"base_url,omitempty" json:"base_url,omitempty"`
	APIKeyEnv      string `yaml:"api_key_env,omitempty" json:"api_key_env,omitempty"`
	APIKey         string `yaml:"api_key,omitempty" json:"-"`
	MaxConcurrency int    `yaml:"max_concurrency,omitempty" json:"max_concurrency,omitempty"`
}

AccountConfig names a provider account without storing credentials.

type AuditConfig

type AuditConfig struct {
	Enabled         bool   `yaml:"enabled,omitempty" json:"enabled,omitempty"`
	RouteEventsFile string `yaml:"route_events_file,omitempty" json:"route_events_file,omitempty"`
	OutcomesFile    string `yaml:"outcomes_file,omitempty" json:"outcomes_file,omitempty"`
}

AuditConfig controls host-side audit file behavior.

type AuditFilter

type AuditFilter struct {
	TaskID string
}

AuditFilter limits read-side audit queries.

type BusinessOutcome

type BusinessOutcome string
const (
	BusinessOutcomeSuccess BusinessOutcome = "success"
	BusinessOutcomeFailure BusinessOutcome = "failure"
	BusinessOutcomePartial BusinessOutcome = "partial"
	BusinessOutcomeUnknown BusinessOutcome = "unknown"
)

type Candidate

type Candidate struct {
	Model                     ModelCapability      `json:"model"`
	AccountAlias              string               `json:"account_alias,omitempty"`
	AccountRateLimited        bool                 `json:"account_rate_limited,omitempty"`
	AccountMaxConcurrency     int                  `json:"account_max_concurrency,omitempty"`
	AccountCurrentConcurrency int                  `json:"account_current_concurrency,omitempty"`
	ProviderAvailability      ProviderAvailability `json:"provider_availability,omitempty"`
	ProviderInFlight          int                  `json:"provider_in_flight,omitempty"`
	ProviderMaxConcurrency    int                  `json:"provider_max_concurrency,omitempty"`
	ProviderQuotaRemaining    int                  `json:"provider_quota_remaining,omitempty"`
	ProviderQuotaExhausted    bool                 `json:"provider_quota_exhausted,omitempty"`
	ProviderFailureStreak     int                  `json:"provider_failure_streak,omitempty"`
	ProviderCooldownUntil     time.Time            `json:"provider_cooldown_until,omitempty"`
}

Candidate is one routable model/account option.

func ApplyModelStats

func ApplyModelStats(stats ModelStats, profile TaskProfile, candidates []Candidate) []Candidate

ApplyModelStats returns a candidate copy enriched with task-specific history. Hosts call this explicitly when they want model-stats.json to influence route scoring through ModelCapability recent reliability fields.

func ApplyProviderHealth

func ApplyProviderHealth(snapshot ProviderHealthSnapshot, candidates []Candidate) []Candidate

type CandidateScore

type CandidateScore struct {
	Alias          string         `json:"alias,omitempty"`
	AccountAlias   string         `json:"account_alias,omitempty"`
	Available      bool           `json:"available"`
	Score          int            `json:"score,omitempty"`
	ScoreBreakdown map[string]int `json:"score_breakdown,omitempty"`
	Reason         string         `json:"reason,omitempty"`
}

CandidateScore records why a candidate was accepted, rejected, or ranked.

type CapabilityLevel

type CapabilityLevel string

CapabilityLevel describes the broad capability tier of a model.

const (
	CapabilitySimple   CapabilityLevel = "simple"
	CapabilityBalanced CapabilityLevel = "balanced"
	CapabilityAdvanced CapabilityLevel = "advanced"
)

type Complexity

type Complexity string

Complexity describes how much model capability a task needs.

const (
	ComplexitySimple Complexity = "simple"
	ComplexityMedium Complexity = "medium"
	ComplexityHard   Complexity = "hard"
)

type Config

type Config struct {
	Home     string          `yaml:"home,omitempty" json:"home,omitempty"`
	Audit    AuditConfig     `yaml:"audit,omitempty" json:"audit,omitempty"`
	Accounts []AccountConfig `yaml:"accounts,omitempty" json:"accounts,omitempty"`
	Models   []ModelConfig   `yaml:"models,omitempty" json:"models,omitempty"`
	Routing  RoutingConfig   `yaml:"routing,omitempty" json:"routing,omitempty"`
}

Config is the host-owned llmkit configuration loaded from LLMKIT_HOME. It intentionally stores API key environment variable names, not key values.

func LoadConfig

func LoadConfig(home string) (*Config, error)

LoadConfig reads config.yaml from home and validates the host-owned config.

func LoadConfigFromEnv

func LoadConfigFromEnv(getenv func(string) string) (*Config, error)

LoadConfigFromEnv resolves LLMKIT_HOME in production mode and loads config.yaml from that directory.

func (Config) Candidates

func (c Config) Candidates() []Candidate

Candidates converts configured models and accounts into route candidates.

func (Config) DefaultTaskProfile

func (c Config) DefaultTaskProfile() TaskProfile

DefaultTaskProfile returns routing defaults from config, with library defaults filling omitted fields.

type ContextWindowClass

type ContextWindowClass string

ContextWindowClass describes relative context-window size.

const (
	ContextShort  ContextWindowClass = "short"
	ContextMedium ContextWindowClass = "medium"
	ContextLong   ContextWindowClass = "long"
)

type ErrorClass

type ErrorClass string
const (
	ErrorClassTransient          ErrorClass = "transient_error"
	ErrorClassRateLimited        ErrorClass = "rate_limited"
	ErrorClassTimeout            ErrorClass = "timeout"
	ErrorClassInvalidJSON        ErrorClass = "invalid_json"
	ErrorClassToolCallInvalid    ErrorClass = "tool_call_invalid"
	ErrorClassCapabilityMismatch ErrorClass = "capability_mismatch"
	ErrorClassPolicyBlocked      ErrorClass = "policy_blocked"
	ErrorClassAuth               ErrorClass = "auth_error"
	ErrorClassCanceled           ErrorClass = "canceled"
	ErrorClassConfiguration      ErrorClass = "configuration_error"
	ErrorClassBudgetExceeded     ErrorClass = "budget_exceeded"
	ErrorClassPostCall           ErrorClass = "post_call_error"
	ErrorClassRequestTooLarge    ErrorClass = "request_too_large"
	ErrorClassResponseTooLarge   ErrorClass = "response_too_large"
	ErrorClassUsageMissing       ErrorClass = "usage_missing"
	ErrorClassRedirectBlocked    ErrorClass = "redirect_blocked"
	ErrorClassInvalidResponse    ErrorClass = "invalid_response"
	// ErrorClassProviderBusy marks a candidate HealthStore.Acquire turned
	// away; no call was made.
	ErrorClassProviderBusy ErrorClass = "provider_busy"
	ErrorClassUnknown      ErrorClass = "unknown"
)

type FailureCost

type FailureCost string

FailureCost describes the user-visible cost of choosing a weak model.

const (
	FailureCostLow    FailureCost = "low"
	FailureCostMedium FailureCost = "medium"
	FailureCostHigh   FailureCost = "high"
)

type HealthPolicy

type HealthPolicy struct {
	FailureCooldownThreshold int
	CooldownDuration         time.Duration
	MaxCooldownDuration      time.Duration
	Now                      func() time.Time
}

HealthPolicy configures the circuit breaker. Reaching FailureCooldownThreshold consecutive provider failures opens it for CooldownDuration; each further failure, such as a failed recovery probe, doubles the cooldown up to MaxCooldownDuration. A degraded provider whose last failure is CooldownDuration old is available again: being ranked below healthy providers, it might otherwise never get the success that clears it.

type HealthStore

type HealthStore interface {
	// Acquire atomically admits one call to the candidate's provider. It
	// returns an error wrapping ErrProviderBusy while the circuit is open,
	// while a recovery probe is in flight (half-open admits exactly one call),
	// when quota is exhausted, or when the provider's or the account's
	// concurrency limit is reached. Each successful Acquire is closed by
	// exactly one RecordOutcome or Release.
	Acquire(context.Context, Candidate) error
	// Release closes an Acquire whose call never reached the provider.
	Release(context.Context, Candidate) error
	RecordOutcome(context.Context, TaskOutcome) error
	Snapshot() ProviderHealthSnapshot
}

type HomeMode

type HomeMode string

HomeMode controls whether ResolveHome may use development conveniences.

const (
	HomeModeDevelopment HomeMode = "development"
	HomeModeProduction  HomeMode = "production"
)

type JSONLRecorder

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

JSONLRecorder appends one JSON object per line under its configured home.

func NewJSONLRecorder

func NewJSONLRecorder(home string) (*JSONLRecorder, error)

NewJSONLRecorder creates a JSONL recorder and ensures its target directory exists. It writes route-events.jsonl and outcomes.jsonl under home.

func (*JSONLRecorder) RecordOutcome

func (r *JSONLRecorder) RecordOutcome(ctx context.Context, outcome TaskOutcome) error

RecordOutcome appends a sanitized task outcome to outcomes.jsonl.

func (*JSONLRecorder) RecordRoute

func (r *JSONLRecorder) RecordRoute(ctx context.Context, trace RouteTrace) error

RecordRoute appends a sanitized routing trace to route-events.jsonl.

type LatencyClass

type LatencyClass string

LatencyClass describes observed or configured model latency.

const (
	LatencySlowClass   LatencyClass = "slow"
	LatencyNormalClass LatencyClass = "normal"
	LatencyFastClass   LatencyClass = "fast"
)

type LatencyRequirement

type LatencyRequirement string

LatencyRequirement describes how strongly routing should prefer lower latency. It is a task-side requirement, not a model measurement.

const (
	LatencyNone   LatencyRequirement = "none"
	LatencyNormal LatencyRequirement = "normal"
	LatencyUrgent LatencyRequirement = "urgent"
)

type MemoryHealthStore

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

func NewMemoryHealthStore

func NewMemoryHealthStore(policy HealthPolicy) *MemoryHealthStore

func (*MemoryHealthStore) Acquire

func (s *MemoryHealthStore) Acquire(ctx context.Context, candidate Candidate) error

func (*MemoryHealthStore) RecordOutcome

func (s *MemoryHealthStore) RecordOutcome(ctx context.Context, outcome TaskOutcome) error

func (*MemoryHealthStore) Release

func (s *MemoryHealthStore) Release(_ context.Context, candidate Candidate) error

func (*MemoryHealthStore) Set

func (s *MemoryHealthStore) Set(entry ProviderHealthEntry)

func (*MemoryHealthStore) Snapshot

type ModelCapability

type ModelCapability struct {
	Alias              string             `json:"alias,omitempty"`
	Provider           string             `json:"provider,omitempty"`
	IsLocal            bool               `json:"is_local,omitempty"`
	CapabilityLevel    CapabilityLevel    `json:"capability_level"`
	SupportsTools      bool               `json:"supports_tools,omitempty"`
	SupportsJSON       bool               `json:"supports_json,omitempty"`
	ContextWindowClass ContextWindowClass `json:"context_window_class"`
	PriceClass         PriceClass         `json:"price_class"`
	LatencyClass       LatencyClass       `json:"latency_class"`
	EstimatedCents     int                `json:"estimated_cents,omitempty"`
	MaxConcurrency     int                `json:"max_concurrency,omitempty"`
	CurrentConcurrency int                `json:"current_concurrency,omitempty"`
	// RecentOutcomes and RecentFailures are outcome counts, optionally
	// weighted by age (see ModelStatsEntry), that drive reliability scoring.
	RecentOutcomes      float64 `json:"recent_outcomes,omitempty"`
	RecentFailures      float64 `json:"recent_failures,omitempty"`
	RecentFailureRate   float64 `json:"recent_failure_rate,omitempty"`
	RecentLatencyMillis int     `json:"recent_latency_ms,omitempty"`
}

ModelCapability is the static and current-runtime capability surface needed by deterministic routing. Task 4 will own route scoring and selection.

func (ModelCapability) Matches

func (m ModelCapability) Matches(profile TaskProfile) bool

Matches reports whether the model can legally handle the profile. It only applies hard capability filters; preference and scoring are routing concerns.

type ModelConfig

type ModelConfig struct {
	Alias              string             `yaml:"alias,omitempty" json:"alias,omitempty"`
	ModelName          string             `yaml:"model,omitempty" json:"model,omitempty"`
	Provider           string             `yaml:"provider,omitempty" json:"provider,omitempty"`
	AccountAlias       string             `yaml:"account_alias,omitempty" json:"account_alias,omitempty"`
	IsLocal            bool               `yaml:"is_local,omitempty" json:"is_local,omitempty"`
	CapabilityLevel    CapabilityLevel    `yaml:"capability_level,omitempty" json:"capability_level,omitempty"`
	SupportsTools      bool               `yaml:"supports_tools,omitempty" json:"supports_tools,omitempty"`
	SupportsJSON       bool               `yaml:"supports_json,omitempty" json:"supports_json,omitempty"`
	ContextWindowClass ContextWindowClass `yaml:"context_window_class,omitempty" json:"context_window_class,omitempty"`
	PriceClass         PriceClass         `yaml:"price_class,omitempty" json:"price_class,omitempty"`
	LatencyClass       LatencyClass       `yaml:"latency_class,omitempty" json:"latency_class,omitempty"`
	EstimatedCents     int                `yaml:"estimated_cents,omitempty" json:"estimated_cents,omitempty"`
	MaxConcurrency     int                `yaml:"max_concurrency,omitempty" json:"max_concurrency,omitempty"`
	Tags               []string           `yaml:"tags,omitempty" json:"tags,omitempty"`
}

ModelConfig describes one configured model/account route candidate.

type ModelStats

type ModelStats struct {
	GeneratedAt time.Time                  `json:"generated_at"`
	Models      map[string]ModelStatsEntry `json:"models"`
}

ModelStats is the generated summary of routing and outcome audit records.

func BuildModelStats

func BuildModelStats(home string) (*ModelStats, error)

BuildModelStats reads audit JSONL files and aggregates route/outcome facts.

func LoadModelStats

func LoadModelStats(home string) (*ModelStats, error)

LoadModelStats reads model-stats.json from home.

func RefreshModelStats

func RefreshModelStats(home string) (*ModelStats, error)

RefreshModelStats rebuilds model-stats.json from route-events.jsonl and outcomes.jsonl under home.

type ModelStatsEntry

type ModelStatsEntry struct {
	TaskType        string  `json:"task_type,omitempty"`
	AccountAlias    string  `json:"account_alias,omitempty"`
	ModelAlias      string  `json:"model_alias,omitempty"`
	Provider        string  `json:"provider,omitempty"`
	RouteAttempts   int     `json:"route_attempts"`
	OutcomeCount    int     `json:"outcome_count"`
	PendingOutcomes int     `json:"pending_outcomes"`
	Successes       int     `json:"successes"`
	Failures        int     `json:"failures"`
	SuccessRate     float64 `json:"success_rate"`
	FailureRate     float64 `json:"failure_rate"`
	// RecentOutcomes and RecentFailures weight each outcome by its age,
	// halving every ModelStatsHalfLife, so routing reacts to how a model
	// behaves now rather than over its whole history.
	RecentOutcomes    float64    `json:"recent_outcomes"`
	RecentFailures    float64    `json:"recent_failures"`
	AvgLatencyMillis  int        `json:"avg_latency_ms,omitempty"`
	AvgInputTokens    int        `json:"avg_input_tokens,omitempty"`
	AvgOutputTokens   int        `json:"avg_output_tokens,omitempty"`
	AvgEstimatedCents int        `json:"avg_estimated_cents,omitempty"`
	LastSeen          *time.Time `json:"last_seen,omitempty"`
}

ModelStatsEntry summarizes one task/model/account/provider route bucket.

type PriceClass

type PriceClass string

PriceClass describes the relative cost of using a model.

const (
	PriceFree   PriceClass = "free"
	PriceLow    PriceClass = "low"
	PriceMedium PriceClass = "medium"
	PriceHigh   PriceClass = "high"
)

type PrivacyLevel

type PrivacyLevel string

PrivacyLevel describes whether a task may leave the host machine.

const (
	PrivacyLocalPreferred PrivacyLevel = "local_preferred"
	PrivacyCloudAllowed   PrivacyLevel = "cloud_allowed"
	PrivacyLocalOnly      PrivacyLevel = "local_only"
)

type ProfileDefaults

type ProfileDefaults struct {
	Complexity  Complexity         `yaml:"complexity,omitempty" json:"complexity,omitempty"`
	Latency     LatencyRequirement `yaml:"latency_requirement,omitempty" json:"latency_requirement,omitempty"`
	FailureCost FailureCost        `yaml:"failure_cost,omitempty" json:"failure_cost,omitempty"`
	Privacy     PrivacyLevel       `yaml:"privacy_level,omitempty" json:"privacy_level,omitempty"`
}

ProfileDefaults maps routing defaults to TaskProfile fields.

type ProfileSource

type ProfileSource string

ProfileSource records whether the host provided the task profile or llmkit filled in defaults.

const (
	ProfileSourceDefault ProfileSource = "default"
	ProfileSourceHost    ProfileSource = "host"
)

type ProviderAvailability

type ProviderAvailability string
const (
	ProviderAvailable   ProviderAvailability = "available"
	ProviderDegraded    ProviderAvailability = "degraded"
	ProviderUnavailable ProviderAvailability = "unavailable"
)

type ProviderHealthEntry

type ProviderHealthEntry struct {
	AccountAlias   string               `json:"account_alias,omitempty"`
	ModelAlias     string               `json:"model_alias,omitempty"`
	Provider       string               `json:"provider,omitempty"`
	Availability   ProviderAvailability `json:"availability,omitempty"`
	InFlight       int                  `json:"in_flight,omitempty"`
	MaxConcurrency int                  `json:"max_concurrency,omitempty"`
	QuotaRemaining int                  `json:"quota_remaining,omitempty"`
	QuotaExhausted bool                 `json:"quota_exhausted,omitempty"`
	FailureStreak  int                  `json:"failure_streak,omitempty"`
	LastFailureAt  time.Time            `json:"last_failure_at,omitempty"`
	CooldownUntil  time.Time            `json:"cooldown_until,omitempty"`
	UpdatedAt      time.Time            `json:"updated_at,omitempty"`
}

type ProviderHealthSnapshot

type ProviderHealthSnapshot struct {
	GeneratedAt time.Time                      `json:"generated_at,omitempty"`
	Entries     map[string]ProviderHealthEntry `json:"entries,omitempty"`
}

type Recorder

type Recorder interface {
	RecordRoute(context.Context, RouteTrace) error
	RecordOutcome(context.Context, TaskOutcome) error
}

Recorder persists routing and outcome audit records.

type RouteAuditRecord

type RouteAuditRecord struct {
	Route   RouteTrace   `json:"route"`
	Outcome *TaskOutcome `json:"outcome,omitempty"`
}

RouteAuditRecord joins one route trace with its outcome when present.

func ReadRouteAudits

func ReadRouteAudits(home string, filter AuditFilter) ([]RouteAuditRecord, error)

ReadRouteAudits reads route audit records from home and joins matching outcomes by route_id and attempt. It is a read-only helper for host observability APIs.

type RouteDecision

type RouteDecision struct {
	Selected       Candidate        `json:"selected"`
	SelectedAlias  string           `json:"selected_alias,omitempty"`
	Score          int              `json:"score"`
	ScoreBreakdown map[string]int   `json:"score_breakdown,omitempty"`
	Reason         string           `json:"reason,omitempty"`
	Candidates     []CandidateScore `json:"candidates"`
}

RouteDecision is the deterministic routing result and its traceable explanation.

type RoutePolicy

type RoutePolicy struct {
	Now func() time.Time
}

RoutePolicy selects a candidate using hard filters followed by stable deterministic scoring.

func (RoutePolicy) Select

func (p RoutePolicy) Select(profile TaskProfile, candidates []Candidate) (RouteDecision, error)

Select returns the best candidate for profile, or a clear error when no candidate can legally handle the task.

type RouteTrace

type RouteTrace struct {
	RouteID               string           `json:"route_id,omitempty"`
	TaskID                string           `json:"task_id,omitempty"`
	Attempt               int              `json:"attempt,omitempty"`
	RecordedAt            time.Time        `json:"recorded_at,omitempty"`
	TaskType              string           `json:"task_type,omitempty"`
	TaskProfile           *TaskProfile     `json:"task_profile,omitempty"`
	AccountAlias          string           `json:"account_alias,omitempty"`
	ModelAlias            string           `json:"model_alias,omitempty"`
	Provider              string           `json:"provider,omitempty"`
	Selected              bool             `json:"selected,omitempty"`
	Reason                string           `json:"reason,omitempty"`
	Score                 int              `json:"score,omitempty"`
	ScoreBreakdown        map[string]int   `json:"score_breakdown,omitempty"`
	CandidateModelAliases []string         `json:"candidate_model_aliases,omitempty"`
	Candidates            []CandidateScore `json:"candidates,omitempty"`
	FallbackMaxAttempts   int              `json:"fallback_max_attempts,omitempty"`
}

RouteTrace is the allowlisted audit record for a routing decision. It stores aliases and routing explanations only; host-owned secrets must not be added.

type RoutingConfig

type RoutingConfig struct {
	Intent   map[string]string `yaml:"intent,omitempty" json:"intent,omitempty"`
	Defaults ProfileDefaults   `yaml:"defaults,omitempty" json:"defaults,omitempty"`
	Rules    []RoutingRule     `yaml:"rules,omitempty" json:"rules,omitempty"`
}

RoutingConfig stores declarative routing defaults and host-owned rule text. The current library consumes Defaults; concrete rule evaluation is future work.

type RoutingRule

type RoutingRule struct {
	When   map[string]string `yaml:"when,omitempty" json:"when,omitempty"`
	Prefer []string          `yaml:"prefer,omitempty" json:"prefer,omitempty"`
}

RoutingRule preserves host-owned declarative preferences from config.yaml.

type SuccessSignal

type SuccessSignal string
const (
	SuccessSignalSchemaPassed  SuccessSignal = "schema_passed"
	SuccessSignalTestsPassed   SuccessSignal = "tests_passed"
	SuccessSignalHumanAccepted SuccessSignal = "human_accepted"
	SuccessSignalToolCompleted SuccessSignal = "tool_completed"
)

type TaskOutcome

type TaskOutcome struct {
	RouteID         string          `json:"route_id,omitempty"`
	TaskID          string          `json:"task_id,omitempty"`
	Attempt         int             `json:"attempt,omitempty"`
	RecordedAt      time.Time       `json:"recorded_at,omitempty"`
	TaskType        string          `json:"task_type,omitempty"`
	AccountAlias    string          `json:"account_alias,omitempty"`
	ModelAlias      string          `json:"model_alias,omitempty"`
	Provider        string          `json:"provider,omitempty"`
	Success         bool            `json:"success"`
	BusinessOutcome BusinessOutcome `json:"business_outcome,omitempty"`
	SuccessSignal   SuccessSignal   `json:"success_signal,omitempty"`
	FailureReason   string          `json:"failure_reason,omitempty"`
	ErrorCode       string          `json:"error_code,omitempty"`
	ErrorClass      ErrorClass      `json:"error_class,omitempty"`
	LatencyMillis   int             `json:"latency_ms,omitempty"`
	InputTokens     int             `json:"input_tokens,omitempty"`
	OutputTokens    int             `json:"output_tokens,omitempty"`
	EstimatedCents  int             `json:"estimated_cents,omitempty"`
}

TaskOutcome is the allowlisted audit record for the result of an LLM task. It records outcome metadata, not prompts, responses, API keys, or headers.

type TaskProfile

type TaskProfile struct {
	TaskType          string             `json:"task_type,omitempty"`
	Source            ProfileSource      `json:"source,omitempty"`
	Complexity        Complexity         `json:"complexity"`
	Latency           LatencyRequirement `json:"latency_requirement"`
	FailureCost       FailureCost        `json:"failure_cost"`
	Privacy           PrivacyLevel       `json:"privacy_level"`
	MaxEstimatedCents int                `json:"max_estimated_cents,omitempty"`
	NeedsReasoning    bool               `json:"needs_reasoning,omitempty"`
	NeedsTools        bool               `json:"needs_tools,omitempty"`
	NeedsJSON         bool               `json:"needs_json,omitempty"`
	NeedsLongContext  bool               `json:"needs_long_context,omitempty"`
}

TaskProfile is the host-provided or default task description used by later routing steps.

func DefaultTaskProfile

func DefaultTaskProfile() TaskProfile

DefaultTaskProfile returns the conservative baseline profile used when the host does not provide a more specific task profile.

Directories

Path Synopsis
adapters
goagent
Package goagent adapts llmkit routing to goagent's LLMClient port.
Package goagent adapts llmkit routing to goagent's LLMClient port.
examples
goagent-routing command

Jump to

Keyboard shortcuts

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