Langertha-Knarr
view release on metacpan or search on metacpan
t/40_streaming_live.t view on Meta::CPAN
use strict;
use warnings;
use Test2::V0;
use IO::Async::Loop;
use Net::Async::HTTP;
use HTTP::Request;
use JSON::MaybeXS;
use Future::AsyncAwait;
use Langertha::Knarr;
use Langertha::Knarr::Handler::Code;
my $json = JSON::MaybeXS->new( utf8 => 1, canonical => 1 );
my $handler = Langertha::Knarr::Handler::Code->new(
code => sub { 'hello world' },
stream_code => sub {
my @parts = ('hel', 'lo ', 'wor', 'ld');
return sub { @parts ? shift @parts : undef };
},
);
my $loop = IO::Async::Loop->new;
my $sb = Langertha::Knarr->new(
handler => $handler,
loop => $loop,
host => '127.0.0.1',
port => 0, # any free port
);
$sb->start;
# Discover the actual port we bound to
my $sock = $sb->_server->read_handle;
my $port = $sock->sockport;
ok( $port, "bound to port $port" );
my $http = Net::Async::HTTP->new;
$loop->add($http);
# --- 1) non-streaming OpenAI roundtrip ---
{
my $body = $json->encode({
model => 'm',
messages => [ { role => 'user', content => 'hi' } ],
});
my $req = HTTP::Request->new( POST => "http://127.0.0.1:$port/v1/chat/completions" );
$req->header( 'Content-Type' => 'application/json' );
$req->content($body);
my $resp = $http->do_request( request => $req )->get;
is( $resp->code, 200, 'non-stream 200' );
my $data = $json->decode( $resp->decoded_content );
is( $data->{object}, 'chat.completion', 'non-stream object' );
is( $data->{choices}[0]{message}{content}, 'hello world', 'non-stream content' );
}
# --- 2) streaming OpenAI ---
{
my $body = $json->encode({
model => 'm',
messages => [ { role => 'user', content => 'hi' } ],
stream => JSON::MaybeXS::true(),
});
my $req = HTTP::Request->new( POST => "http://127.0.0.1:$port/v1/chat/completions" );
$req->header( 'Content-Type' => 'application/json' );
$req->content($body);
my $resp = $http->do_request( request => $req )->get;
is( $resp->code, 200, 'stream 200' );
is( $resp->header('Content-Type'), 'text/event-stream', 'SSE content type' );
my $body_text = $resp->decoded_content;
my @data_lines = grep { /^data:/ } split /\n/, $body_text;
ok( scalar(@data_lines) >= 4, 'multiple SSE data lines emitted' );
like( $body_text, qr/hel/, 'first chunk present' );
like( $body_text, qr/ld/, 'last chunk present' );
}
# --- 3) streaming Ollama (NDJSON) ---
{
my $body = $json->encode({
model => 'm',
messages => [ { role => 'user', content => 'hi' } ],
stream => JSON::MaybeXS::true(),
});
my $req = HTTP::Request->new( POST => "http://127.0.0.1:$port/api/chat" );
$req->header( 'Content-Type' => 'application/json' );
$req->content($body);
my $resp = $http->do_request( request => $req )->get;
is( $resp->code, 200, 'ollama stream 200' );
is( $resp->header('Content-Type'), 'application/x-ndjson', 'NDJSON content type' );
my @lines = grep { length } split /\n/, $resp->decoded_content;
ok( scalar(@lines) >= 4, 'multiple ndjson lines' );
my $first = $json->decode($lines[0]);
is( $first->{message}{content}, 'hel', 'first ollama chunk' );
my $last = $json->decode($lines[-1]);
ok( $last->{done}, 'last ollama chunk has done:true' );
}
done_testing;
( run in 1.666 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )