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 )