Revision history for Langertha-Knarr
1.101 2026-08-15 00:13:51Z
- Fixed: the routed path died on any real engine response that
reported usage. Knarr::Response declared usage as Maybe[Object],
but Langertha::Response carries a plain Maybe[HashRef] and engines
write the provider's raw JSON hash straight through — so the
constructor rejected it and took the request with it. Present
since 1.100. It went unnoticed because passthrough is the default
(only explicitly configured models take the routed path) and
because t/27_usage_routing.t hand-built a Langertha::Usage object
that no engine ever produces. Knarr::Response now upgrades a raw
usage hashref via Langertha::Usage->from_hash in BUILDARGS, which
covers every door into the object: coerce() on a {content,usage}
hashref and clone_with re-running new() were two further ways into
the same crash. The type is not weakened, and an already-blessed
usage passes through as the same instance. Note the 1.100 entry
above describes a ToolCall/Usage symmetry that never existed on
the usage side — Langertha upgrades tool_calls to value objects
in BUILDARGS but leaves usage a hashref.
- Fixed: with Langfuse configured, a routed response carrying usage
then died a second time, in the Langfuse batch encode. Langertha's
value objects have no TO_JSON, and Tracing::flush encoded without
an eval, so a serialization failure inside end_trace turned an
already-answered, already-paid-for upstream call into a 500 — and
on the streaming path, where the trace closes after the last
chunk, into a client left waiting for an end marker. The JSONL
request log hit the same object but encodes inside an eval that
only warns, so it silently dropped whole log lines instead.
Usage is now flattened at the JSON boundary in both Tracing and
RequestLog, mirroring the _rate_limit_hash that already sat next
to it. Neither defect was reachable before, because the
constructor died first.
- Tracing::flush no longer throws. A failed encode is logged at
error level and the batch dropped, exactly as an ingestion HTTP
failure twenty lines below already was. The batch is detached
from _batch before the encode and unrecoverable either way, so
dying bought nothing and cost the request; the asymmetry between
"Langfuse is down" (warn) and "we built a payload we cannot
serialize" (fatal) was accidental.
- Model defaults are read off the Langertha engine classes instead
of being hardcoded. Config.pm carried two identical %default_models
hashes, in from_env and generate_config, both dating from April
and partly dead — deepseek-chat was retired by DeepSeek on
2026-07-24, so a generated config pointed at a model that no
longer exists. A single default_model_for() now asks
$engine_class->default_model, which cannot drift. Fixed three
further stale values on the way (Mistral large -> small, MiniMax
M2.1 -> M2.7, Cerebras llama-3.3-70b -> llama3.1-8b) and picked up
claude-sonnet-5 / gpt-5.6-terra / deepseek-v4-flash for free on a
newer Langertha. Groq and OpenRouter keep an explicit fallback
because their engine classes deliberately croak rather than name a
default; the test asserts they still do, so the fallback is itself
drift-monitored.
- Environment scanning knows 19 engines, up from 12: XAI, Moonshot,
NousResearch, AKI, Scaleway, TSystems and Hetzner. Hetzner is
detected only via LANGERTHA_HETZNER_API_KEY — the bare
HETZNER_API_KEY is in wide use for the Hetzner Cloud
infrastructure API and would false-positive into an unusable model
entry. Protocol variants sharing a vendor's credential
(MoonshotAnthropic, MiniMaxAnthropic, AKIOpenAI, OpenAIResponses)
stay out, since one key would emit two model stanzas for one
provider, and local engines reached by URL rather than by key
(LMStudio, vLLM, SGLang, LlamaCpp, Ollama, Whisper) have nothing
to detect. New engine_catalog class method exposes the list.
- Langfuse generations carry real latency. Langertha::Response grew
timing with ttft_seconds / total_seconds, but Knarr dropped it
along with id, thinking and rate_limit when normalizing. The
generation's endTime is now startTime + total_seconds and
completionStartTime (the Langfuse field for time-to-first-token)
is startTime + ttft_seconds, both anchored to the high-resolution
instant start_trace recorded rather than to a wall clock that has
moved on; the raw timing hash is kept in metadata so provider-native
stage durations survive. Only the routed non-streaming path has
engine-measured timing — streaming and raw passthrough keep the
proxy's own wall clock, and a new POD section says which path
measures what. rate_limit is flattened to its quota scalars, never
its raw headers.
- response_format is capability-gated like every other generation
parameter, and gated on the right one: Langertha registers
response_format_json_object and response_format_json_schema
separately, so an explicit json_schema demands the schema flag
while OpenAI's json_object and Ollama's bare 'json' need only the
object flag. Gating both on the schema flag would have dropped a
plain json_object request on an engine that can only do the loose
form.
- Documentation overhaul: README, example-config.yaml and module
POD updated to the current model defaults (gpt-5.6-terra,
claude-sonnet-5, deepseek-v4-flash, MiniMax-M3, ...), the full
19-engine provider and environment-variable tables, all config
options (logging, langfuse trace_name, api_key_env, generation
defaults), and examples for the less obvious features (A2A/ACP/
AG-UI clients, Handler::Code fakes, passthrough). The example
config no longer mixes explicit and default model keys without
comment, and bin/knarr documents --log-file / --log-dir.
1.100 2026-04-26 20:36:43Z
- Langertha floor 0.500 — for ToolCall/Usage value objects,
capability registry, and chat_f named-args entry point.
- New Langertha::Knarr::Response value object: single shape every
handler returns and every protocol formatter consumes. Carries
content, model, usage (Langertha::Usage), tool_calls
(ArrayRef[Langertha::ToolCall]), finish_reason, raw. coerce()
upgrades legacy returns (bare string, {content,model} hashref,
Langertha::Response) so handlers can hand back whatever is
convenient and the dispatcher normalizes once at the boundary.
11 blessed/ref-HASH triplets across the handler/decorator/protocol
tree replaced with a single typed call site.
- Generation parameters now reach the engine. Handler::Engine and
Handler::Router call $engine->chat_f(messages=>..., tools=>...,
tool_choice=>..., response_format=>..., temperature=>...,
max_tokens=>...) instead of the old simple_chat_f(@msgs) which
silently dropped everything. Knarr::Request gained chat_f_args($engine)
that builds the named-arg list capability-aware (consults
$engine->supports($cap) so unsupported params are dropped before
reaching engines that would reject them).
- Knarr::Request gained tool_choice and response_format as
first-class attributes; OpenAI/Anthropic/Ollama parsers populate
them from the wire body.
- Tool calls are surfaced in proxy responses. Configured (non-
passthrough) routes that produce tool_calls now serialize them
into OpenAI message.tool_calls (with finish_reason: tool_calls),
Anthropic content[] tool_use blocks (stop_reason adapts), and
Ollama message.tool_calls. Previous behaviour: silently dropped.
- Real usage in proxy responses. When the engine reports a
Langertha::Usage, OpenAI / Anthropic / Ollama formatters serialize
via to_openai_format / to_anthropic_format / to_ollama_format
instead of emitting hardcoded zeros. Tracing's end_trace gets the
Usage object too — Langfuse generations now carry real token
counts.
- Tracing flush is async via Net::Async::HTTP. The previous
LWP::UserAgent call blocked the IO::Async event loop on every
end_trace; the new flush fires the request and returns
immediately, with a warn-on-fail logger attached.
- Streaming pump consolidated into Knarr::Stream::from_callback.
Removes ~50 lines of duplicated queue/pending/finished/error
bookkeeping from Handler::Engine and Handler::Router.
- supports()-aware streaming detection. The old
$engine->can('simple_chat_stream_realtime_f') heuristic now
defers to $engine->supports('streaming') when available
(Langertha 0.500+) and falls back to the can()-check for older
engines.
- Dead steerboard attribute removed from all six Protocol classes
(never read), and Knarr.pm no longer threads $self into
protocol-object construction. Knarr::PSGI's constructor argument
renamed steerboard => knarr.
- 'steerboard' string fallbacks replaced with 'unknown' (model
default) and 'knarr-code' / 'knarr-raider' (handler defaults).
- Removed unused handle_embedding_f / handle_transcription_f stubs
from the Handler role (no protocol parser, no test, no caller —
revisit when a concrete embedding/transcription routing strategy
exists).
- Tests grew from 343 to 360. New: t/15_response.t (value-object
contract), t/25_chat_f_params.t (param forwarding + capability
gating + tool_calls survival through Engine handler),
t/26_tool_calls_routing.t (OpenAI/Anthropic/Ollama formatter
output), t/27_usage_routing.t (usage serializer roundtrip).
1.001 2026-04-12 22:22:19Z
- Raw passthrough: unconfigured models are now piped 1:1 as raw
HTTP bytes to the upstream API. All protocol metadata (tool_use,
usage, cache_control, stop_reason, content block indices) is
preserved. Client auth headers are forwarded transparently.
- Langfuse tracing for passthrough requests: every raw passthrough
call creates a Langfuse trace with model, protocol, and timing.
- 'knarr container' is now a deprecated alias for
'knarr start --from-env'. The --from-env flag builds config from
environment variables when no config file is found.
- --port is now repeatable: -p 8080 -p 11434 binds multiple ports.
--host defaults to 0.0.0.0.
- KNARR_DEBUG=1 environment variable enables verbose logging
(equivalent to --verbose / -v).
- Logging always active: Log::Any::Adapter::Stderr is now always
set (warning level by default, trace level with --verbose).
Errors in request handling are logged to stderr.
- Anthropic system prompt: handle array-of-content-blocks format
sent by Claude Code (was: only plain string).
- Removed route_model from Handler role — decorator chain
(Tracing, RequestLog) is no longer bypassed.
- Dockerfile rewritten: cpm instead of cpanm, cpanfile-based
installation, non-root user, Docker-cache-friendly layers.
- Bump Langertha floor to 0.401.
- Added IO::Async::SSL to cpanfile (required for HTTPS passthrough).
- Test suite expanded to 343 tests. New coverage:
* Raw passthrough with mock Langfuse tracing
* Passthrough with and without tracing enabled
- Documentation updated: CLAUDE.md, README.md, bin/knarr POD
reflect new architecture, --from-env, repeatable --port,
KNARR_DEBUG, raw passthrough behavior.
1.000 2026-04-10 00:00:20Z
- MAJOR REWRITE. Mojolicious is gone; the new core is built on
IO::Async + Net::Async::HTTP::Server + Future::AsyncAwait for
native async streaming and seamless integration with Langertha
engines. Existing knarr.yaml configs and the knarr CLI commands
(start, container, models, check, init) keep working.
- Six wire protocols loaded by default on every listening port:
OpenAI (/v1/chat/completions, /v1/models, SSE), Anthropic
(/v1/messages, named-event SSE), Ollama (/api/chat, /api/tags,
NDJSON streaming), A2A (Google Agent2Agent JSON-RPC at / with
/.well-known/agent.json discovery), ACP (BeeAI/IBM /agents,
/runs), and AG-UI (CopilotKit /awp event stream).
- Pluggable Handler architecture replaces the old Knarr::Proxy
classes:
Knarr::Handler::Router — model→engine via Knarr::Router
Knarr::Handler::Engine — single Langertha engine wrapper
Knarr::Handler::Raider — per-session Langertha::Raider
Knarr::Handler::Code — coderef-backed for tests/fakes
Knarr::Handler::Passthrough — raw HTTP forward to upstream
Knarr::Handler::A2AClient — consume remote A2A agents
Knarr::Handler::ACPClient — consume remote ACP agents
Knarr::Handler::Tracing — Langfuse tracing decorator
Knarr::Handler::RequestLog — JSONL request logging decorator
- Langfuse tracing and per-request JSONL logging restored as
decorator handlers and auto-mounted by knarr start / knarr
container when their respective config sections are present.
- Multi-listen restored: the listen attribute is always an
arrayref (default composed from host + port), supports any
number of host:port entries. knarr container binds 8080 +
11434 simultaneously so existing Ollama clients work without
reconfiguration.
- proxy_api_key authentication enforced by the new dispatcher.
Clients must present 'Authorization: Bearer <token>' or
'x-api-key: <token>' when KNARR_API_KEY (or yaml proxy_api_key)
is set. The /.well-known/agent.json discovery route stays
anonymous.
- PSGI adapter (Knarr::PSGI) for deploying behind any Plack
server (Starman, Twiggy, mod_perl, ...). Streaming is buffered
in this mode; use the native server for real-time streaming.
- Universal protocol translator pattern: an OpenAI-fronted Knarr
with a Handler::A2AClient backend exposes a remote A2A agent to
OpenAI clients (and similarly for ACP).
- Drops the Knarr namespace facades for Langertha utility code
(Knarr::Metrics, Knarr::Input, Knarr::Output, Knarr::Input::Tools,
Knarr::Output::Tools). Use Langertha::Usage / Tool / ToolCall /
ToolChoice / Pricing / Cost / UsageRecord directly from
Langertha core.
- cpanfile drops Mojolicious, Test::Mojo, MooseX::Role::Parameterized,
HTTP::Message, File::ShareDir::ProjectDistDir; adds IO::Async,
Net::Async::HTTP, Net::Async::HTTP::Server, Future::AsyncAwait,
Moose, Path::Tiny (test), Capture::Tiny (test).
- Bump Langertha floor to 0.400 for the new value object API.
- dist.ini sets irc = #langertha (on irc.perl.org).
- Test suite expanded from 200 to 320 unit tests + author POD
syntax. New coverage:
* end-to-end live streaming for Anthropic / A2A / ACP / AG-UI
(OpenAI and Ollama already covered)
* Handler::Passthrough sync + streaming through a real backend
* Handler::Router with passthrough fallback
* Multi-listen on real sockets
* Tracing and RequestLog decorators
* CLI smoketest for knarr models / check / init
* proxy_api_key auth enforcement
0.007 2026-03-10 20:05:00Z
- Resolve configured engines through both `Langertha::Engine::*` and
`LangerthaX::Engine::*` (including fully-qualified class names).
- Add router test coverage for `LangerthaX::Engine::*` custom engines.
- Tighten dependency minimums in cpanfile:
`Langertha 0.307`, `Mojolicious 9.0`.
- Update README and POD docs to describe custom engine resolution order.
- Dockerfile: add optional `LANGERTHA_SRC` build arg to inject unreleased
Langertha source when CPAN indexers lag.
- dist.ini release Docker build supports optional
`KNARR_DOCKER_BUILD_ARGS='...'` passthrough for temporary build overrides,
including GitHub release-dist URL examples and a note against
`/archive/refs/*` source archives for dzil dists.
- dist.ini now uploads `Langertha-Knarr-%v.tar.gz` to GitHub releases
(`Getty/langertha-knarr`) via `gh` during post-release hooks.
- dist.ini now runs `${GH_BIN:-gh}` directly in separate `run_after_release`
steps (no pre-check, no chained `&&`), so hooks use invoking-shell runtime
environment/path.
0.005 2026-03-10 14:48:05Z
- Add primary Knarr normalization modules (core-backed):
`Langertha::Knarr::Input`, `Langertha::Knarr::Output`, `Langertha::Knarr::Metrics`.
- Bump `Langertha` dependency to `>= 0.306`.
- Add request policy hooks in `build_app`:
- `before_request` hook to inspect/mutate/block requests before routing
- `api_key_validator` hook for dynamic per-request key authorization
- Add cross-format tool bridging between OpenAI and Anthropic request/response shapes
(including `tools`, `tool_choice`, `tool_calls`, `tool_use`, and `tool_result`)
- Add Hermes tool XML bridging:
- Parse `<tool_call>{...}</tool_call>` into OpenAI `tool_calls`
- Parse `<tool_call>{...}</tool_call>` into Anthropic `tool_use` blocks
- Route OpenAI `tools` through Hermes-capable engines by normalizing tool schema
- Extend proxy/unit/integration tests for hook behavior and tool-bridge paths
0.004 2026-03-03 05:22:38Z
- Fix routing priority: passthrough before default engine fallback
- Add passthrough-only mode (no API keys needed)
- Add KNARR_TRACE_NAME / --trace-name for custom Langfuse trace names
- Support LANGFUSE_BASE_URL env var (in addition to LANGFUSE_URL)
- Strip quotes from env values (Docker --env-file compatibility)
- Strip Accept-Encoding/Content-Encoding in passthrough (no gzip issues)
- Rewrite POD: passthrough proxy as primary use case
- Fix Langertha dependency version to 0.303
- Fix author URL
0.001 2026-03-03 04:00:32Z
- Initial release
- Multi-format LLM proxy: OpenAI, Anthropic, Ollama
- Passthrough mode with Langfuse tracing
- Docker container mode with auto-detect from ENV
- CLI: start, container, models, check, init