Langertha

 view release on metacpan or  search on metacpan

lib/Langertha/Engine/Gemini.pm  view on Meta::CPAN

  # Gemini response format: candidates[0].content.parts[].text
  my $candidates = $data->{candidates} || [];
  my $text = '';
  my $finish_reason;
  my $thinking;
  if (@$candidates) {
    my $candidate = $candidates->[0];
    my $content = $candidate->{content} || {};
    my $parts = $content->{parts} || [];
    my @text_parts;
    my @thought_parts;
    for my $part (@$parts) {
      next unless exists $part->{text};
      if ($part->{thought}) {
        push @thought_parts, $part->{text};
      } else {
        push @text_parts, $part->{text};
      }
    }
    $text = join('', @text_parts);
    $thinking = join("\n", @thought_parts) if @thought_parts;
    $finish_reason = $candidate->{finishReason};
  }

  # Normalize Gemini usage metadata
  my $usage;
  if (my $um = $data->{usageMetadata}) {
    $usage = {
      prompt_tokens     => $um->{promptTokenCount},
      completion_tokens => $um->{candidatesTokenCount},
      total_tokens      => $um->{totalTokenCount},
    };
  }

  my @tcs = Langertha::ToolCall->extract($data);
  return Langertha::Response->new(
    content       => $text,
    raw           => $data,
    $data->{modelVersion} ? ( model => $data->{modelVersion} ) : (),
    defined $finish_reason ? ( finish_reason => $finish_reason ) : (),
    $usage ? ( usage => $usage ) : (),
    defined $thinking ? ( thinking => $thinking ) : (),
    @tcs ? ( tool_calls => [ @tcs ] ) : (),
  );
}

sub stream_format { 'sse' }

sub chat_stream_request {
  my ( $self, $messages, %extra ) = @_;

  # Same tool_choice translation as chat_request.
  if ( exists $extra{tool_choice} && defined $extra{tool_choice} ) {
    my $tc = Langertha::ToolChoice->from_hash( delete $extra{tool_choice} );
    if ($tc) {
      my $cfg = $tc->to_gemini;
      $extra{toolConfig} = $cfg if $cfg;
    }
  }

  # Convert messages to Gemini format (same as non-streaming)
  my @gemini_contents;
  my $system_instruction;

  for my $message (@{$messages}) {
    if ($message->{role} eq 'system') {
      $system_instruction .= "\n\n" if $system_instruction;
      $system_instruction .= $message->{content};
    } else {
      my $role = $message->{role} eq 'assistant' ? 'model' : $message->{role};
      push @gemini_contents, {
        role => $role,
        parts => [{ text => $message->{content} }],
      };
    }
  }

  # Build the URL for streaming endpoint
  my $model_name = $self->chat_model;
  my $url = $self->url . "/v1beta/models/${model_name}:streamGenerateContent?key=" . $self->api_key . "&alt=sse";

  my %request_body = (
    contents => \@gemini_contents,
  );

  if ($system_instruction) {
    $request_body{systemInstruction} = {
      parts => [{ text => $system_instruction }],
    };
  }

  my %generation_config;
  if ($self->get_response_size) {
    $generation_config{maxOutputTokens} = $self->get_response_size;
  }
  if ($self->has_temperature) {
    $generation_config{temperature} = $self->temperature;
  }

  $request_body{generationConfig} = \%generation_config if %generation_config;

  return $self->generate_http_request(
    POST => $url,
    sub {},
    %request_body,
    %extra,
  );
}

sub parse_stream_chunk {
  my ( $self, $data, $event ) = @_;

  require Langertha::Stream::Chunk;

  # Gemini streaming format is similar to non-streaming
  my $candidates = $data->{candidates} || [];
  return undef unless @$candidates;

  my $candidate = $candidates->[0];
  my $content = $candidate->{content} || {};
  my $parts = $content->{parts} || [];

  my $text = '';
  $text = $parts->[0]->{text} if @$parts && $parts->[0]->{text};

  my $finish_reason = $candidate->{finishReason};
  my $is_final = defined $finish_reason && $finish_reason ne '';

  return Langertha::Stream::Chunk->new(
    content => $text,
    raw => $data,
    is_final => $is_final,
    $finish_reason ? (finish_reason => $finish_reason) : (),
    $data->{usageMetadata} ? (usage => $data->{usageMetadata}) : (),
  );
}

# Dynamic model listing with token pagination
sub list_models_request {
  my ($self, %params) = @_;
  my $url = $self->url . '/v1beta/models?key=' . $self->api_key;

  # Add pagination params if provided
  if (%params) {
    require URI;
    my $uri = URI->new($url);
    my %query = $uri->query_form;
    $uri->query_form(%query, %params);
    $url = $uri->as_string;
  }

  return $self->generate_http_request(
    GET => $url,
    sub { $self->list_models_response(shift) },
  );
}

sub list_models_response {
  my ($self, $response) = @_;
  my $data = $self->parse_response($response);
  return $data;
}

sub _fetch_all_models {
  my ($self) = @_;
  my @all_models;
  my $page_token;

  do {
    my $request = $self->list_models_request(
      $page_token ? (pageToken => $page_token) : ()
    );
    my $response = $self->user_agent->request($request);
    my $data = $request->response_call->($response);



( run in 2.321 seconds using v1.01-cache-2.11-cpan-0bb4e1dffa6 )