LLM::Chat::Test::Kit

NAME

LLM::Chat::Test::Kit - fixture-server plumbing shared by the HTTP-level tests

SYNOPSIS

use LLM::Chat::Test::Kit;

my $server = raw-server(-> $request {
    given $request.method, $request.path {
        when ('POST', '/v1/chat/completions') {
            $request.json<stream>
                ?? %( type => 'text/event-stream', chunks => @sse-frames )
                !! %( body => to-json(%reply) );
        }
        default { %( status => 404, body => '' ) }
    }
});
LEAVE $server.stop;

my $backend = LLM::Chat::Backend::OpenAICommon.new(
    api_url  => $server.url ~ '/v1',
    settings => LLM::Chat::Backend::Settings.new,
);

my $response = stream-settled($backend.chat-completion-stream(@messages), 'the stream');

DESCRIPTION

The HTTP-level tests need the same few things: a real HTTP server on a port nothing else owns, a way to wait for a Promise that gives up loudly instead of hanging the suite, and a way to drive a LLM::Chat::Backend::Response to its terminal state. Those live here so the files that need them stay about the behaviour under test.

The server is raw-server: a small HTTP/1.1 server written directly on IO::Socket::Async, which finishes writing a response before it closes anything. Not Cro's own HTTP server, because every client this distribution makes asks the server to close the connection after its response (Connection: close), and on that path Cro's server occasionally loses the end of a response on a machine that is busy enough: a streamed body cut short at a chunk boundary, or never sent at all. Nearly every assertion an HTTP-level test makes rests on the whole response arriving.

This is test support, not library surface. It ships inside the dist's own namespace (rather than under t/lib) deliberately, so that zef install's test phase precompiles it as part of lib/ staging before any test file runs. A t/lib CompUnit repo starts cold on a fresh zef install, and parallel test files pulling the fixture's dependency chain into that cold store can race and fail the install outright; moving test support into lib/ under a ::Test namespace is the fleet-wide fix for that failure class.

Requests and answers

raw-server listens on a port the OS picks, on 127.0.0.1, and calls the responder with a RawRequest for each request — .method, .path (without the query string), .query, .headers (names lower-cased), .header($name), .body (a Blob), .text, .json, .wants-close — and the responder returns a Hash describing the answer:

  • status — default 200.

  • type — the Content-Type, written exactly as given; default application/json. An undefined type sends no Content-Type at all.

  • headers — further headers, a list of Pairs.

  • body — a Str (sent as UTF-8) or a Blob, sent with a Content-Length.

  • chunks — instead of a body: a list, or a Supply, of Strs or Blobs, each sent as one HTTP chunk. A Supply is followed for as long as it emits; when the client hangs up first, the Supply's tap is closed, so its CLOSE phaser is where a test learns that it happened.

  • delay — seconds to wait before the response headers, or a Promise to wait for (a server that answers when the test lets it).

A bare Str or Blob is short for %( body = ... )>. An answer the server cannot use, or a responder that dies, is answered with a 500 and recorded in .errors, so a fixture mistake fails the test instead of hanging it. Connections are kept open between requests unless the request asks for Connection: close; then the server closes the connection once the response's last write has completed, as RFC 9112 requires. .requests lists what arrived, .accepted and .open-connections count connections, .url is the base URL to point a backend at, and .stop closes everything.

A server that streams forever and notices the client hanging up:

my $hung-up = Promise.new;
my $server = raw-server(-> $ {
    %( type => 'text/event-stream', chunks => supply {
        whenever Supply.interval(0.01) -> $n { emit "data: tick $n\n\n" }
        CLOSE { $hung-up.keep(True) unless $hung-up.status }
    } );
});

LLM::Chat v0.10.3

Simple framework for LLM inferencing

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Cro::HTTP::Client:ver<0.8.11+>:auth<zef:cro>Digest::SHA256::Native:ver<1.0.0+>:auth<zef:bduggan>JSON::Fast:ver<0.21.0+>:auth<zef:timo>Template::Jinja2:ver<0.3.0+>:auth<zef:apogee>Tokenizers:ver<0.3.0+>:auth<zef:apogee>UUID::V4:ver<1.0.0+>:auth<zef:masukomi>

Provides

  • LLM::Chat
  • LLM::Chat::Backend
  • LLM::Chat::Backend::KoboldCpp
  • LLM::Chat::Backend::Mock
  • LLM::Chat::Backend::OpenAICommon
  • LLM::Chat::Backend::OpenRouter
  • LLM::Chat::Backend::Response
  • LLM::Chat::Backend::Response::OpenRouter
  • LLM::Chat::Backend::Response::OpenRouter::Augment
  • LLM::Chat::Backend::Response::OpenRouter::Stream
  • LLM::Chat::Backend::Response::Stream
  • LLM::Chat::Backend::Settings
  • LLM::Chat::Conversation
  • LLM::Chat::Conversation::Message
  • LLM::Chat::Debug
  • LLM::Chat::ExceptionText
  • LLM::Chat::Retry
  • LLM::Chat::Retry::Exceptions
  • LLM::Chat::Template
  • LLM::Chat::Template::ChatML
  • LLM::Chat::Template::DeepSeekV4
  • LLM::Chat::Template::Gemma2
  • LLM::Chat::Template::Jinja2
  • LLM::Chat::Template::Llama3
  • LLM::Chat::Template::Llama4
  • LLM::Chat::Template::MistralV7
  • LLM::Chat::Test::Kit
  • LLM::Chat::TokenCounter
  • LLM::Chat::ToolLoop
  • LLM::Chat::WireJSON

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.