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
