Langertha

 view release on metacpan or  search on metacpan

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

  my $model = $self->embedding_model;

  my %config = (
    defined $self->embedding_dimensions ? ( outputDimensionality => $self->embedding_dimensions ) : (),
    %{ delete $extra{embedContentConfig} // {} },
  );
  for my $key ( sort keys %EMBED_CONFIG_KEY ) {
    $config{ $EMBED_CONFIG_KEY{$key} } = delete $extra{$key} if exists $extra{$key};
  }
  my $embed_request = sub {
    my ( $text ) = @_;
    return {
      model   => 'models/' . $model,
      content => { parts => [ { text => $text } ] },
      %config ? ( embedContentConfig => {%config} ) : (),
      %extra,
    };
  };

  if ( ref $input eq 'ARRAY' ) {
    return $self->generate_http_request(
      POST => $self->gemini_model_url( $model, 'batchEmbedContents' ),
      sub { $self->embedding_response( shift, $input ) },
      requests => [ map { $embed_request->($_) } @{$input} ],
    );
  }
  return $self->generate_http_request(
    POST => $self->gemini_model_url( $model, 'embedContent' ),
    sub { $self->embedding_response( shift, $input ) },
    %{ $embed_request->($input) },
  );
}


sub embedding_response {
  my ( $self, $response, $input ) = @_;
  my $data = $self->parse_response($response);
  my $batch = ref $input eq 'ARRAY';
  my @embeddings = ref $data ne 'HASH' ? ()
    : $batch ? ( ref $data->{embeddings} eq 'ARRAY' ? @{ $data->{embeddings} } : () )
    : ( defined $data->{embedding} ? $data->{embedding} : () );
  my @vectors = map { ref $_ eq 'HASH' ? $_->{values} : undef } @embeddings;
  # No vector is no result: never hand back undef as if it were one (k290).
  if ( !@vectors || grep { ref $_ ne 'ARRAY' || !@{$_} } @vectors ) {
    my $err = ref $data eq 'HASH' && $data->{error}
      ? ( ref $data->{error} eq 'HASH' ? $data->{error}{message} : $data->{error} )
      : undef;
    croak "".(ref $self)." embedding response contained no vector"
      . ( defined $err ? " (error: $err)" : '' );
  }
  return $vectors[0] unless $batch;
  croak "".(ref $self)." embedding response returned ".scalar(@vectors)
    ." vectors for ".scalar(@{$input})." inputs"
    unless @vectors == @{$input};
  return \@vectors;
}


sub update_request {
  my ( $self, $request ) = @_;
  $request->header('content-type', 'application/json');
}

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

  # 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};
  }
  else {
    # No candidate: a blocked prompt says why in promptFeedback.blockReason.
    # A block is an answer, so it is reported as finish_reason, verbatim like
    # a candidate's finishReason (SAFETY, PROHIBITED_CONTENT, ...). Anything
    # else without a candidate is no answer and croaks. -- karr k301
    $finish_reason = $self->_gemini_block_reason($data);
    unless ( defined $finish_reason ) {
      my $error = $self->_body_error_text( $data->{error} );
      croak "".(ref $self)." response carried an error: $error" if defined $error;
      croak "".(ref $self)." response contained no candidates";
    }
  }

  # Normalize Gemini usage metadata. cachedContentTokenCount is surfaced
  # when present so callers can monitor cache-hit rate (karr #22, 22e).
  # See https://ai.google.dev/api/generate-content (usageMetadata).
  # Langertha::Usage->from_hash reads both spellings (ADR 0018 tier 1), but the
  # rename stays: Usage's %{} overload serves this hash verbatim, so
  # $response->usage->{prompt_tokens} / {cached_content_token_count} are
  # public back-compat keys (Langertha::CachedContent POD, karr k197).
  # The snake_case keys take OpenAI's meaning: thoughtsTokenCount (billed as
  # output, not part of candidatesTokenCount) is counted in completion_tokens
  # and shown under completion_tokens_details.reasoning_tokens; the tool-use
  # prompt (toolUsePromptTokenCount) in prompt_tokens (k299).
  my $usage;
  if (my $um = $data->{usageMetadata}) {
    my $thoughts = $um->{thoughtsTokenCount};
    my ( $prompt, $tool_prompt, $candidates ) =
      @{$um}{qw( promptTokenCount toolUsePromptTokenCount candidatesTokenCount )};
    $usage = {
      prompt_tokens     => defined $tool_prompt ? ( $prompt // 0 ) + $tool_prompt : $prompt,



( run in 0.859 second using v1.01-cache-2.11-cpan-99fa8243a42 )