NAME
Langertha::Virtual::Server - HTTP endpoint in front of a Langertha::Engine::Virtual
VERSION
version 0.504
SYNOPSIS
use Langertha::Engine::Virtual;
use Langertha::Engine::OpenAI;
use Langertha::Virtual::Server;
use Langertha::Virtual::Rules qw( answer tool after );
my $engine = Langertha::Engine::Virtual->new(
rules => [ 'Hello' => answer('Hi there!') ],
);
# In your own process, inside your event loop
my $loop = IO::Async::Loop->new;
my $server = Langertha::Virtual::Server->new( engine => $engine, loop => $loop );
$server->start;
my $client = Langertha::Engine::OpenAI->new( url => $server->url, api_key => 'x' );
my $text = $loop->await( $client->simple_chat_f('Hello') );
$server->stop;
# In its own process, for programs that take an upstream URL
my ( $pid, $url ) = Langertha::Virtual::Server->spawn( engine => $engine );
system( 'some-program', '-o', "url=$url" );
Langertha::Virtual::Server->reap($pid);
DESCRIPTION
Serves a Langertha::Engine::Virtual over HTTP on 127.0.0.1, so a program that reaches its upstream by URL (a gateway, a node, an agent with url=...) can run against a rule-driven engine without network or key. The reply to each request comes from the engine's own rule evaluation ("virtual_reply" in Langertha::Engine::Virtual); this class only speaks HTTP. The engine keeps the record ("requests" in Langertha::Engine::Virtual).
Endpoints:
POST /v1/chat/completions- an OpenAI chat completion withtool_calls, andusagewhen the engine's reaction or "default_usage" in Langertha::Engine::Virtual gives one; with"stream": truethe same reply as server-sent events (chat.completion.chunkevents, thendata: [DONE]). The reply is computed whole and then cut into events by "virtual_stream_chunks" in Langertha::Engine::Virtual: the text in pieces of the engine's "stream_chunk_size" in Langertha::Engine::Virtual characters, each tool call as an opening fragment followed by its arguments in pieces, thefinish_reason, and with"stream_options": { "include_usage": true }a last event with an emptychoiceslist and theusage. All events go out in one write.GET /v1/models- the engine's model, in the OpenAI list format.GET /_virtual/requests- only with "debug":{ requests => [...], calls => [...] }as the engine recorded them, which is how a test reads them when the server runs in another process.anything else -
404with an OpenAI-style JSON error body.
An error reaction is the response with its status and { error => { message, type, code } }; a request no rule answers is 500, a body that is not JSON 400. The Anthropic dialect (/v1/messages) is not served.
The server is single-threaded and answers one request after another, closing each connection. Two ways to run it: in your event loop (give loop, an IO::Async::Loop; the client must then also be asynchronous, a synchronous call in the same process would wait for a server that cannot run), or in its own process with "spawn".
engine
The Langertha::Engine::Virtual (or Mock) that answers. Required.
port
The TCP port on 127.0.0.1; 0 (the default) takes a free one, readable from "url" after "start".
loop
An IO::Async::Loop to serve in. Without it, the server only runs through "run" or "spawn". IO::Async is optional for Langertha; it is loaded only when loop is set.
debug
Serve GET /_virtual/requests. Off by default.
delay
Seconds to wait before answering each chat request, so a client with a timeout runs into it. Default 0. In "run" and "spawn" the wait blocks the whole server; in a loop only that response waits.
url
my $url = $server->url; # http://127.0.0.1:PORT/v1
The base URL for url => ... of an engine or program, with /v1 like every OpenAI-dialect engine's. Croaks before "start".
start
Binds the listening socket (returns the server). With a loop the server is then served by it; otherwise call "run".
stop
Stops serving and closes the listening socket and every open connection. Safe to call twice.
run
$server->start->run;
Serves in a blocking loop until the process ends. With parent => $pid it also returns once that process is no longer the parent, so a spawned server does not outlive a test that died. Croaks if the server has a loop.
spawn
my ( $pid, $url ) = Langertha::Virtual::Server->spawn( engine => $engine, debug => 1 );
Class method. Takes the constructor arguments (a loop is ignored), binds the port, forks, and serves from the child. Returns the child's pid and the "url", ready to be connected to. The child works on its own copy of the engine, so the parent's "requests" in Langertha::Engine::Virtual stay empty: use debug and GET /_virtual/requests. End it with "reap".
reap
Langertha::Virtual::Server->reap($pid);
Class method. Ends a "spawn"ed server and waits for it, so no zombie stays.
SEE ALSO
Langertha::Engine::Virtual - The engine and its rule language
Langertha::Virtual::Rules -
answer,tool,tools,after,errorLangertha::Engine::OpenAI - A client for the endpoint, with
url => $server->url
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha/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 <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.