NAME

Langertha::Knarr::Config - YAML configuration loader and validator

VERSION

version 1.103

SYNOPSIS

use Langertha::Knarr::Config;

# Load from file
my $config = Langertha::Knarr::Config->new(file => 'knarr.yaml');

# Build from environment (Docker zero-config mode)
my $config = Langertha::Knarr::Config->from_env;

# Validate
my @errors = $config->validate;
die join("\n", @errors) if @errors;

DESCRIPTION

Loads and validates Knarr configuration from a YAML file or from environment variables. All string values in the YAML file support ${ENV_VAR} interpolation.

The attributes below are the configuration file reference: each one names its YAML key and, where there is one, its environment variable. The README has an annotated example, and share/example-config.yaml in the distribution is a commented starting point.

file

Path to the YAML configuration file. Optional — when omitted the config object starts with an empty data hash (useful for testing or for configs built purely from environment variables via "from_env").

data

The raw configuration hashref, loaded from "file" and with all ${ENV_VAR} references expanded. Can be supplied directly to bypass file loading.

engine_catalog

my $defs = Langertha::Knarr::Config->engine_catalog;
for my $def (@$defs) { say $def->{engine}, ": @{$def->{vars}}" }

Class method. Returns an ArrayRef of {engine, vars} hashrefs describing every engine "scan_env" can auto-detect, with its API key environment variable names in priority order. The list is a copy — mutating it does not affect scanning.

default_model_for

my $model = Langertha::Knarr::Config->default_model_for('Anthropic');

Class method. Returns the default model name for an engine, read from the Langertha engine class itself (Langertha::Engine::Anthropic->default_model) so the catalog cannot drift away from the framework. Returns undef when the engine class is not installed, or when it has no default model and no fallback is known. The result is cached per engine name.

from_env

my $config = Langertha::Knarr::Config->from_env(%opts);

Class method. Builds a config object purely from environment variables (zero-config Docker mode). Calls "scan_env" to detect which API keys are set, assigns each detected engine its "default_model_for" model, enables auto_discover and passthrough, and sets OpenAI as the default engine when an OpenAI key is present. Every generated entry, the default engine included, names the variable its key was found in as api_key_env, so OPENAI_API_KEY works as well as LANGERTHA_OPENAI_API_KEY.

Options are passed through to "scan_env" (e.g. include_test).

listen

ArrayRef of host:port strings to listen on (listen:, a list or a single string). Defaults to ['127.0.0.1:8080', '127.0.0.1:11434'], so a config without listen: only answers on loopback. knarr start -p replaces it with the given ports on -H (default 0.0.0.0); the Docker image starts that way.

models

HashRef of model name → model definition hashref from the models: config section. Each definition may include engine, model, api_key_env, api_key, url, system_prompt, temperature, response_size, context_size, and user_agent_timeout (seconds the engine waits for its upstream; defaults to "upstream_timeout", 0 disables it).

context_size is passed to engines that compose Langertha::Role::ContextSize (Ollama, LMStudio native), which send it on the wire (Ollama's num_ctx, LM Studio's context_length) and report it from /api/show. For any other engine it is ignored, with one warning when the models are loaded.

Models found by "auto_discover" inherit the endpoint-level keys of the entry they were discovered through, but not the model-specific model and context_size; see "DESCRIPTION" in Langertha::Knarr::Router for the split.

default_engine

HashRef from the default: config section, or undef if not set. At minimum contains engine. Used as the fallback when a model name is not explicitly configured and no passthrough URL matches -- that is, no "passthrough" upstream exists for the protocol the client speaks (Ollama without an ollama entry, and A2A, ACP and AG-UI always). Without a default engine such a request is answered with 404 in the client protocol's error shape.

A model in this section is what the default engine uses for a request that names no model (A2A always, ACP without agent_name); a model the client names replaces it. The section takes the same keys as a "models" entry, api_key_env or api_key included; without either the engine reads only its own LANGERTHA_*_API_KEY variable.

log_file

Path to a JSONL log file for request logging. Resolved from logging.file in config or KNARR_LOG_FILE environment variable.

log_dir

Path to a directory for per-request JSON log files. Resolved from logging.dir in config or KNARR_LOG_DIR environment variable.

langfuse

HashRef from the langfuse: config section. May contain url, public_key, secret_key, trace_name, transport (see "langfuse_transport") and timeout (see "langfuse_timeout"). Returns an empty hashref when the section is absent.

langfuse_transport

How Langertha::Knarr::Tracing sends traces to Langfuse: ingestion (the default) posts trace and generation events to Langfuse's /api/public/ingestion API; otel exports them as OpenTelemetry spans (OTLP/HTTP, JSON encoded) to /api/public/otel/v1/traces, the path Langfuse now recommends (Langfuse Cloud, or self-hosted v3.22 and later; Langfuse v2 has only the ingestion API). See "OpenTelemetry transport" in Langertha::Knarr::Tracing. Read from langfuse.transport, falling back to the KNARR_LANGFUSE_TRANSPORT environment variable, so it also applies under --from-env. Case does not matter. Any other value croaks when read, and "validate" reports it.

langfuse_timeout

Seconds a trace POST to Langfuse may take before it is given up and logged as a Langfuse flush error warning, for either "langfuse_transport". Default 15; 0 disables it. The POST never holds a request, so this only decides when a slow Langfuse is reported as failed. Read from langfuse.timeout, falling back to the KNARR_LANGFUSE_TIMEOUT environment variable. A value that is not a non-negative number croaks when read, and "validate" reports it.

proxy_api_key

Optional shared secret that clients must present in the Authorization: Bearer or x-api-key header. Falls back to the KNARR_API_KEY environment variable. When not set -- or set to an empty string -- the proxy is open (no auth required); knarr start warns about an explicitly empty key. It is never forwarded to a passthrough upstream; see "auth_token" in Langertha::Knarr.

public_url

Optional public base URL of this Knarr (e.g. https://knarr.example), published in the provider manifest at /.well-known/langertha.json. Falls back to the KNARR_PUBLIC_URL environment variable. When not set, the manifest takes the base URL from each request.

has_proxy_api_key

if ($config->has_proxy_api_key) { ... }

Returns true when a "proxy_api_key" is configured.

admin_key

Optional secret for Knarr's admin routes under /knarr/admin/ (see Langertha::Knarr::Admin): the only key they accept, and a key no other route accepts. Falls back to the KNARR_ADMIN_KEY environment variable. Unset or empty: the admin routes do not exist (404). It must be at least "admin_key_min_length" characters and differ from "proxy_api_key"; "validate" reports either. Prefer the environment variable: a key in the config file sits on disk.

has_admin_key

if ($config->has_admin_key) { ... }

True when a non-empty "admin_key" is set.

admin_key_min_length

The shortest "admin_key" Knarr accepts: 16 characters.

remote_config

Boolean, default 0. Lets Knarr start without any model: it then answers every request except the admin routes with 503 (not configured) until a central service delivers the routing setup with PUT /knarr/admin/config. What is in the config file (models, default engine, passthrough, proxy key, ...) is always in effect, with or without remote_config; a delivered setup lies as a layer over it (same model name: the delivered one wins) and is held in memory only, so a restart forgets it. Needs an "admin_key". Falls back to KNARR_REMOTE_CONFIG; knarr start --remote-config sets it too.

token_auth

Optional HashRef, or undef without a token_auth: section: Knarr checks a short-lived token per model against a database view instead of one shared key (see Langertha::Knarr::Authorizer::Token). The attribute holds the section with its defaults filled in:

token_auth:
  backend: postgresql
  dsn: dbi:Pg:host=db;dbname=platform
  user: tester_knarr
  password_env: TESTER_DB_PASSWORD   # name of the variable, never the password
  relation: tester_token_auth        # view or table, optionally schema.name
  listen: tester_token               # NOTIFY channel: issue and revocation
  hash: sha256                       # default; sha256(token) in hex is looked up
  columns: { hash: token_hash, model: model, provider: provider, expires: expires_at }
  match_model: true                  # default

backend must be postgresql; dsn, relation and listen are required. columns is optional with the defaults shown (token_hash, model, provider, expires_at), any other column key is an error, and so is any other key of the section. match_model defaults to true. The password is only ever read from the environment variable password_env names. token_auth needs "routes", and excludes "proxy_api_key", "models", "default_engine", "passthrough", "auto_discover" and "remote_config": see "validate". It is a start-time setting, never part of an admin push.

routes

HashRef of the provider value of a token to the route its requests take, required with "token_auth" and only valid with it:

routes:
  local:     { upstream: http://api:8080/v1, key_env: SKEID_KEY }
  openai:    { provider: openai,    key_env: OPENAI_API_KEY }
  anthropic: { provider: anthropic, key_env: ANTHROPIC_API_KEY, url: https://eu.api.example }

Each route has exactly one of upstream (an OpenAI-compatible endpoint, http:// or https://) and provider (an engine of "engine_catalog", any case), and a key_env: the name of the environment variable that holds the key Knarr uses there (read when a request comes). A provider route may name another endpoint of that provider with url. A token whose provider has no route is refused with 403. See Langertha::Knarr::Handler::TokenRoutes.

runtime_keys

my @keys = Langertha::Knarr::Config->runtime_keys;

The top-level keys an admin push (PUT /knarr/admin/config) may set: models, default, passthrough, auto_discover, proxy_api_key, upstream_timeout, upstream_stall_timeout, probe_capabilities, probe_timeout and langfuse (validated like in the file). Every other key takes effect only at start; logging in particular never arrives by push.

admin_errors

my @errors = $config->admin_errors;

The part of "validate" about "admin_key" and "remote_config": the key's length, that it differs from "proxy_api_key", and that "remote_config" has a key. The messages never contain a key. knarr start runs it for every config, also one built by --from-env.

ollama_compat_version

Optional Ollama version to report at GET /api/version (see "ollama_compat_version" in Langertha::Knarr). Falls back to the KNARR_OLLAMA_COMPAT_VERSION environment variable. When not set, Knarr's default applies. A value that is not three dot-separated numbers (0.34.4) croaks when read, and "validate" reports it: Ollama clients parse each part as an integer.

a2a_name

Optional name of the A2A agent card at /.well-known/agent.json (see "agent_name" in Langertha::Knarr::Protocol::A2A). Resolved from a2a.name in config or the KNARR_A2A_NAME environment variable. When not set, the card says Langertha Knarr Agent.

a2a_description

Optional description of the A2A agent card (see "agent_description" in Langertha::Knarr::Protocol::A2A). Resolved from a2a.description in config or the KNARR_A2A_DESCRIPTION environment variable. When not set, Knarr's default description applies.

protocol_args

my $knarr = Langertha::Knarr->new( ..., protocol_args => $config->protocol_args );

Returns the "protocol_args" in Langertha::Knarr HashRef for the configured protocol settings: { A2A => { agent_name => ..., agent_description => ... } }, with only the keys that are set ("a2a_name", "a2a_description").

upstream_timeout

Seconds an upstream may take to answer a non-streaming request. Default 300; 0 disables it. Falls back to the KNARR_UPSTREAM_TIMEOUT environment variable. It is the total time of a raw passthrough request (answered with 504 in the client protocol's error shape when it runs out), and it becomes the user_agent_timeout of every routed engine whose model config sets none -- Langertha applies that as the total time of a plain request and as the time without data of a streaming one. A value that is not a non-negative number croaks when read, and "validate" reports it.

upstream_stall_timeout

Seconds a streaming raw passthrough request may go without data from the upstream, the wait for its response headers included. Default 120; 0 disables it. Falls back to the KNARR_UPSTREAM_STALL_TIMEOUT environment variable. A stream that stalls before its headers is answered with 504; one that stalls later ends with the protocol's error frame. Croaks when read and is reported by "validate" like "upstream_timeout".

probe_capabilities

Boolean, default 1. When true, "start" in Langertha::Knarr asks every routed engine that can read its provider's model metadata (OpenRouter, Mistral, LM Studio, T-Systems, Ollama, llama.cpp; Langertha core's probe_model_capabilities_f) which capabilities its model has, once at startup, after auto-discovery. What it learns (today: whether the model sees images) then shows as vision in POST /api/show and as image_input in the provider manifest. Engines without such metadata and a Langertha too old to probe (0.503) send nothing. A failed probe is logged and changes nothing. Set 0 (or KNARR_PROBE_CAPABILITIES=0) to never send these requests. See "probe_capabilities_f" in Langertha::Knarr::Router.

probe_timeout

Seconds one capability probe (see "probe_capabilities") may take before it is given up and logged. Default 10; 0 leaves only the engine's own user_agent_timeout. Falls back to KNARR_PROBE_TIMEOUT. A value that is not a non-negative number croaks when read, and "validate" reports it.

workers

Number of processes knarr start serves from. Default 1: one process, nothing forked. With more, it forks that many workers on the same listen sockets and supervises them (see "workers" in Langertha::Knarr). Falls back to KNARR_WORKERS, so it also applies under --from-env; knarr start -w N wins over both. A value that is not a whole number of 1 or more croaks when read, and "validate" reports it.

auto_discover

Boolean. When true, Langertha::Knarr::Router asks each configured endpoint (engine, URL and API key variable) for its model list the first time a model is resolved, making all discovered models available without explicit config entries and listing them (/v1/models, /api/tags, the manifest). A discovered model is routed through its engine, except when the "passthrough" upstream of the client's protocol is the endpoint that listed it (same scheme, host, port and path, an engine's trailing /v1 aside, so a gateway's /openai and /groq paths are two upstreams) and the request carries the client's own provider key (Authorization for OpenAI, x-api-key or Authorization for Anthropic, not counting the "proxy_api_key"; Ollama needs none): then it passes through byte for byte like an unknown model, with that key. Without one it is routed through its engine, with the engine's key, and so it is when the upstream refuses that key with 401 (see "raw_passthrough" in Langertha::Knarr). A model under "models" is always routed. Defaults to 0; "from_env" and the knarr init output turn it on.

passthrough

HashRef of format name (openai, anthropic, ollama) → upstream base URL. Empty, and passthrough off, unless the config has a passthrough: key; "from_env" turns it on. passthrough: true in YAML enables openai and anthropic with their default upstream URLs (https://api.openai.com and https://api.anthropic.com); ollama has no default and needs its URL. Per-format URLs can be customised or set to false to disable selectively.

A request for a model that is neither configured nor auto-discovered (or auto-discovered from that very upstream, see "auto_discover") goes to the upstream of its protocol byte for byte, with the client's own headers and key. A protocol without an upstream sends such a request to the "default_engine" instead.

passthrough_url_for

my $url = $config->passthrough_url_for('openai');

Returns the upstream base URL for the given format name (e.g. openai or anthropic), or undef if passthrough is not configured for that format.

validate

my @errors = $config->validate;

Validates the configuration and returns a list of error strings. Returns an empty list when the config is valid. A config file that does not load (not YAML, or not a mapping) is reported alone, and so are the sections models, default, langfuse, logging, a2a and passthrough when one is not a mapping (passthrough may also be true or false), and listen when it is not a string or a list of strings: reading such a section croaks. Otherwise it checks that every model entry is a mapping with an engine key, that the default engine (if set) has an engine key, that at least one model or default engine is configured (not under "remote_config"), that a model's context_size (and the default engine's), when set, is a positive integer, and its user_agent_timeout a non-negative number, and that "ollama_compat_version", when set, is three dot-separated numbers, and that "upstream_timeout", "upstream_stall_timeout" and "probe_timeout" are non-negative numbers, and that "workers" is a whole number of 1 or more, plus everything "admin_errors" checks.

With a "token_auth" section it checks the section (backend postgresql, dsn, relation and listen, hash sha256, known columns keys, a password_env that is a variable name), that every "routes" entry has key_env and exactly one of upstream and provider (a known engine), and reports "token_auth" without routes, routes without "token_auth", and a "token_auth" together with "proxy_api_key" (or KNARR_API_KEY), "models", a "default_engine", "passthrough", "auto_discover" or "remote_config". No model is needed then. None of these messages holds a value of the section, only key names.

scan_env

my $found = Langertha::Knarr::Config->scan_env(
  env_files    => ['.env', '.env.local'],
  include_test => 1,
);

Class method. Scans %ENV and optional .env files for known API key environment variables. Returns a HashRef of engine name → {engine, api_key_env} for every engine whose key was found. See "engine_catalog" for the engines and variable names it knows about.

Priority order per engine: LANGERTHA_*_API_KEY beats the bare vendor key (e.g. OPENAI_API_KEY), which beats the TEST_LANGERTHA_*_API_KEY variant.

Options:

  • env_files — ArrayRef of .env file paths to parse (optional)

  • include_test — Include TEST_* variables (default: 1)

generate_config

my $yaml = Langertha::Knarr::Config->generate_config(
  engines => $found,       # from scan_env
  listen  => ['127.0.0.1:8080', '127.0.0.1:11434'],
);

Class method. Generates a YAML configuration string from scan_env results. The generated YAML includes sensible defaults for each detected engine and commented-out stanzas for optional features. Used by knarr init.

Returns a string.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-knarr/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.