Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/CLI/Ask.pm view on Meta::CPAN
# Output: backend name string.
sub _resolve_backend {
my ( $opts, $transcript ) = @_;
return $opts->{backend} if $opts->{backend} ne '';
return $transcript->{backend} if $transcript->{backend} && $BACKEND_FLAG{ $transcript->{backend} };
return 'claude';
}
# _dispatch_backend(%args)
# Routes one turn to the selected backend and returns its answer text.
# Input: backend, prompt, model, images/text_files array refs, history array
# ref, claude_conf hash ref, env, and the ua/runner/detect seams.
# Output: answer text string; dies on backend failure.
sub _dispatch_backend {
my (%a) = @_;
return _ask_claude(%a) if $a{backend} eq 'claude';
return _ask_nova(%a) if $a{backend} eq 'nova';
return _ask_cli_backend(%a);
}
# _ask_claude(%args)
# Answers via the Anthropic API when a key is resolvable, else falls back to the
# local `claude` CLI (Claude Code).
# Input: same payload as _dispatch_backend.
# Output: answer text string; dies when no key and no CLI are available.
sub _ask_claude {
my (%a) = @_;
my $key = _resolve_api_key( $a{claude_conf}, $a{env} );
if ( $key ne '' ) {
my $messages = _build_api_messages( $a{history}, $a{prompt}, $a{text_files}, $a{images} );
my $ua = $a{ua};
$ua = _default_ua() if !$ua;
my $base_url = $a{claude_conf}{base_url};
$base_url = $DEFAULT_BASE_URL if !$base_url;
my $model = $a{model};
$model = $DEFAULT_MODEL if !$model;
my $max_tokens = $a{claude_conf}{max_tokens};
$max_tokens = $DEFAULT_MAX_TOKENS if !$max_tokens;
return _call_claude_api(
ua => $ua,
key => $key,
base_url => $base_url,
model => $model,
max_tokens => $max_tokens,
messages => $messages,
root => $a{paths}->current_project_root,
);
}
die "Image attachments need an ANTHROPIC_API_KEY; the local claude CLI fallback cannot attach images.\n"
if @{ $a{images} };
my $cli = $a{detect}->('claude')
or die "No ANTHROPIC_API_KEY set and no `claude` CLI found. Set the key or install Claude Code.\n";
my $prompt = _compose_cli_prompt( $a{history}, $a{prompt}, $a{text_files} );
my @argv = ( command_argv_for_path($cli), '-p', $prompt, '--output-format', 'text' );
push @argv, ( '--model', $a{model} ) if defined $a{model};
return _capture_backend( 'claude', \@argv, $a{runner} );
}
# _ask_nova(%args)
# Answers via Amazon Nova's own standalone REST endpoint (DD-952) -
# api.nova.amazon.com/v1/chat/completions, bearer-token auth. This is
# architecturally identical to _ask_claude's direct-API path (see
# docs/dashboard-ask-backend-architecture.md), never AWS Bedrock/SigV4 -
# NOVA_API_KEY is a plain bearer token, not an AWS credential.
# Input: same payload as _dispatch_backend.
# Output: answer text string; dies when no NOVA_API_KEY is set or images
# are attached (Nova's image content-block shape is not implemented here).
sub _ask_nova {
my (%a) = @_;
my $key = $a{env}{NOVA_API_KEY};
die "No NOVA_API_KEY set. Set it to use --nova.\n" if !defined $key || $key eq '';
die "Image attachments are not supported with --nova.\n" if @{ $a{images} };
my $messages = _build_api_messages( $a{history}, $a{prompt}, $a{text_files}, [] );
my $ua = $a{ua};
$ua = _default_ua() if !$ua;
my $model = $a{model};
$model = $NOVA_DEFAULT_MODEL if !$model;
return _call_nova_api(
ua => $ua,
key => $key,
base_url => $NOVA_DEFAULT_BASE_URL,
model => $model,
messages => $messages,
);
}
# _call_nova_api(%args)
# Posts one request to Nova's chat-completions endpoint and extracts the
# answer text.
# Input: ua, key, base_url, model, and messages array ref.
# Output: answer text string; dies on a non-success HTTP response or an
# unparseable/empty response body.
sub _call_nova_api {
my (%a) = @_;
require HTTP::Request;
my $url = $a{base_url} . '/v1/chat/completions';
my $req = HTTP::Request->new( POST => $url );
$req->header( 'content-type' => 'application/json' );
$req->header( 'authorization' => "Bearer $a{key}" );
$req->content(
json_encode(
{
model => $a{model},
messages => $a{messages},
}
)
);
my $resp = $a{ua}->request($req);
die "Nova API request failed: @{[ $resp->status_line ]}\n" if !$resp->is_success;
return _extract_nova_api_text( json_decode( $resp->decoded_content ) );
}
# _extract_nova_api_text($data)
# Extracts the answer text from a Nova chat-completions response
# (OpenAI-chat-completions shape: {choices:[{message:{content}}]}), a
# genuinely different response body than Claude's {content:[...]} shape.
# Input: decoded response hash ref.
# Output: answer text string; dies when no text content is present.
sub _extract_nova_api_text {
my ($data) = @_;
die "Nova API returned no content.\n"
if ref($data) ne 'HASH' || ref( $data->{choices} ) ne 'ARRAY' || !@{ $data->{choices} };
my $content = $data->{choices}[0]{message}{content};
die "Nova API returned no text.\n" if !defined $content || $content eq '';
return $content;
}
# _ask_cli_backend(%args)
# Answers via a shelled-out CLI backend (codex/copilot/gemini), forcing a
# read-only, non-interactive invocation and attaching images natively.
# Input: same payload as _dispatch_backend.
# Output: answer text string; dies when the backend CLI is missing or fails.
sub _ask_cli_backend {
my (%a) = @_;
my $name = $a{backend};
my $cli = $a{detect}->($name)
or die _missing_backend_message($name);
my $prompt = _compose_cli_prompt( $a{history}, $a{prompt}, $a{text_files} );
my @base = command_argv_for_path($cli);
my @argv;
if ( $name eq 'codex' ) {
@argv = ( @base, 'exec', '-s', 'read-only', '--skip-git-repo-check', '--color', 'never' );
push @argv, ( '--model', $a{model} ) if defined $a{model};
push @argv, ( '-i', $_ ) for @{ $a{images} };
push @argv, ( '--', $prompt );
}
( run in 0.745 second using v1.01-cache-2.11-cpan-16aabcbfbcb )