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 )