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 with tool_calls, and usage when the engine's reaction or "default_usage" in Langertha::Engine::Virtual gives one; with "stream": true the same reply as server-sent events (chat.completion.chunk events, then data: [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, the finish_reason, and with "stream_options": { "include_usage": true } a last event with an empty choices list and the usage. 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 - 404 with 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

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.