Langertha-Knarr

 view release on metacpan or  search on metacpan

lib/Langertha/Knarr/Handler/Router.pm  view on Meta::CPAN

use Future;
use Future::AsyncAwait;
use Langertha::Knarr::Stream;
use Langertha::Knarr::Response;

with 'Langertha::Knarr::Handler';


# Wraps a Langertha::Knarr::Router (which is Moo) and uses it to resolve
# incoming model names to Langertha engine instances. Also keeps the
# upstream Knarr::Config visible for the rest of the request lifecycle.

has router => ( is => 'ro', required => 1 );

# Optional Passthrough handler used as fallback when the router can't
# resolve a model. Allows mixed mode: configured models go via Langertha
# engines (with tracing/middleware support), unknown models tunnel straight
# to the upstream API the client thinks they're talking to.
has passthrough => (
  is => 'ro',
  isa => 'Maybe[Object]',
  default => sub { undef },
);

sub _resolve {
  my ($self, $model) = @_;
  $model //= 'default';
  if ($self->passthrough) {
    # With passthrough: try without default engine first so unknown models
    # go to passthrough instead of being routed to the default engine.
    my @r = eval { $self->router->resolve($model, skip_default => 1) };
    return @r if @r;
    return ();  # not found or error → passthrough
  }
  my @r = eval { $self->router->resolve($model) };
  return @r unless $@;
  die $@;
}

async sub handle_chat_f {
  my ($self, $session, $request) = @_;
  my ($engine, $canonical_model) = $self->_resolve( $request->model );
  unless ( $engine ) {
    return Langertha::Knarr::Response->coerce(
      await $self->passthrough->handle_chat_f( $session, $request )
    );
  }
  my $response = await $engine->chat_f( $request->chat_f_args($engine) );
  my $r = Langertha::Knarr::Response->coerce($response);
  return $r->clone_with( model => $canonical_model );
}

async sub handle_stream_f {
  my ($self, $session, $request) = @_;
  my ($engine) = $self->_resolve( $request->model );

  unless ( $engine ) {
    return await $self->passthrough->handle_stream_f( $session, $request );
  }

  unless ( _supports_streaming($engine) ) {
    my $r = await $self->handle_chat_f($session, $request);
    return Langertha::Knarr::Stream->from_list( $r->content );
  }

  return Langertha::Knarr::Stream->from_callback( sub {
    my ($emit, $done, $fail) = @_;
    my $cb = sub {
      my ($chunk) = @_;
      my $text = ref $chunk && $chunk->can('content') ? $chunk->content : "$chunk";
      $emit->($text);
    };
    my $f = $engine->simple_chat_stream_realtime_f( $cb, @{ $request->messages } );
    $f->on_done( $done );
    $f->on_fail( $fail );
    $f->retain;
  });
}

sub _supports_streaming {
  my ($engine) = @_;
  return $engine->supports('streaming') if $engine->can('supports');
  return $engine->can('simple_chat_stream_realtime_f') && $engine->can('chat_stream_request');
}

sub list_models {
  my ($self) = @_;
  my $models = $self->router->list_models;
  return [ map { ref $_ eq 'HASH' ? $_ : { id => "$_", object => 'model' } } @{ $models || [] } ];
}

__PACKAGE__->meta->make_immutable;
1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Langertha::Knarr::Handler::Router - Knarr handler that resolves model names via Langertha::Knarr::Router and dispatches to engines

=head1 VERSION

version 1.100

=head1 SYNOPSIS

    use Langertha::Knarr::Config;
    use Langertha::Knarr::Router;
    use Langertha::Knarr::Handler::Router;
    use Langertha::Knarr::Handler::Passthrough;

    my $config = Langertha::Knarr::Config->new(file => 'knarr.yaml');
    my $router = Langertha::Knarr::Router->new(config => $config);

    my $handler = Langertha::Knarr::Handler::Router->new(
        router      => $router,
        passthrough => Langertha::Knarr::Handler::Passthrough->new(
            upstreams => $config->passthrough,
        ),
    );

=head1 DESCRIPTION

Resolves incoming model names against a L<Langertha::Knarr::Router>
(which knows your C<knarr.yaml>) and dispatches to the matched
L<Langertha::Engine>. When a passthrough fallback handler is supplied,
unknown model names tunnel through to it instead of failing — this
preserves the classic Knarr behaviour where configured models go via
Langertha and everything else passes straight to the upstream API.

Streaming responses are pumped via the engine's
C<simple_chat_stream_realtime_f> for native token-by-token delivery.

=head2 router

Required. A L<Langertha::Knarr::Router> instance.

=head2 passthrough



( run in 0.597 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )