Langertha

 view release on metacpan or  search on metacpan

lib/Langertha/Role/AnthropicCompatible.pm  view on Meta::CPAN

package Langertha::Role::AnthropicCompatible;
# ABSTRACT: Role for Anthropic-compatible API format
our $VERSION = '0.505';
use Moose::Role;
use Carp qw( croak carp );
use JSON::MaybeXS;
use Langertha::ToolChoice;
use Langertha::Tool;
use Langertha::Response;
use Langertha::ToolCall;


sub _build_reasoning_wire_format { 'anthropic' }
sub _build_cache_wire_format { 'anthropic' }

sub default_response_size { 1024 }

sub content_format { 'anthropic' }

has api_key => (
  is => 'ro',
  lazy_build => 1,
);
sub _build_api_key {
  my ( $self ) = @_;
  return croak "".(ref $self)." requires api_key to be set";
}


has api_version => (
  is => 'ro',
  lazy_build => 1,
);
sub _build_api_version { '2023-06-01' }


has effort => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_effort',
);


has inference_geo => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_inference_geo',
);


sub update_request {
  my ( $self, $request ) = @_;
  $request->header('x-api-key', $self->api_key);
  $request->header('content-type', 'application/json');
  $request->header('anthropic-version', $self->api_version);
}


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

  # Canonical per-request controls (chat_f, karr #46) beat the engine
  # attributes on a per-key basis; the rest of %extra passes straight through.
  my $controls = delete $extra{controls} // {};

  # Structured output. Engines whose wire has native structured output
  # (Engine::Anthropic, via _native_structured_output) emit it as
  # output_config.format and leave the content JSON on the wire (chat_response
  # returns it verbatim). Engines without it (the legacy /anthropic shims)
  # keep the ADR 0005 synthesized-tool + forced tool_choice rewrite, whose
  # tool_use input chat_response lifts back into content.
  my $rf_routed = 0;
  my $output_config_format;
  if ( $self->_native_structured_output_for_model ) {
    my $rf = $self->_take_response_format(\%extra, $controls);
    # A schema-bearing response_format goes native (output_config.format); the
    # first-party validator rejects an open schema, so it is normalized to a
    # closed one first (k182/k149). A bare json_object has no schema and the
    # Messages API has no native free-form JSON form, so it routes through the
    # synthesized-tool path (open, non-strict input_schema) exactly like the
    # legacy /anthropic shims -- forced where the model allows it, degraded to
    # tool_choice `auto` where forced tool use 400s (fable-5-1 / mythos-5-1).
    if ( $self->_rf_is_native_schema($rf) ) {
      $output_config_format = $self->_response_format_to_output_config($rf);
    }
    else {
      # // 0 keeps $rf_routed defined (a non-honorable rf yields undef) so
      # chat_response does not fall back to the has_response_format heuristic.
      $rf_routed = $self->_response_format_via_tool($rf, \%extra) // 0;
    }
  }
  else {
    $rf_routed = $self->_translate_response_format(\%extra, $controls);
  }

  $self->_normalize_tool_params(\%extra, $controls);
  my @msgs;
  my $system = "";
  for my $message (@{$messages}) {
    if ($message->{role} eq 'system') {
      $system .= "\n\n" if length $system;
      $system .= $message->{content};
    } else {
      push @msgs, $message;
    }
  }
  if ($system and scalar @msgs == 0) {
    push @msgs, {
      role => 'user',
      content => $system,
    };
    $system = undef;
  }

lib/Langertha/Role/AnthropicCompatible.pm  view on Meta::CPAN

C<parallel_tool_use>, C<has_response_format> / C<response_format>) — normally
provided by extending L<Langertha::Engine::Remote> plus the universal roles
composed in L<Langertha::Engine::AnthropicBase>.

The wire envelope mirrors L<Langertha::Role::OpenAICompatible>: this role owns
the Anthropic request/response/stream/auth/rate-limit envelope, and the base
class stays a thin composition shell.

=head2 api_key

Anthropic-compatible API key sent as C<x-api-key>. Subclasses typically
override C<_build_api_key> to read a provider-specific environment variable.

=head2 api_version

The Anthropic API version header sent with every request. Defaults to
C<2023-06-01>.

=head2 effort

Back-compat alias of L<Langertha::Role::ReasoningEffort/reasoning_effort>.
Controls the depth of thinking for reasoning models. When set (and
C<reasoning_effort> is not), it seeds C<reasoning_effort>, which is serialized
via L<Langertha::Reasoning> to C<output_config.effort> plus
C<thinking: { type =E<gt> 'adaptive' }> (the current Messages-API shape) rather
than the legacy top-level C<effort> key.

    my $claude = Langertha::Engine::Anthropic->new(
        api_key => $ENV{ANTHROPIC_API_KEY},
        model   => 'claude-opus-4-8',
        effort  => 'high',   # same as reasoning_effort => 'high'
    );

=head2 inference_geo

Controls data residency for inference on the first-party Claude API. The API
accepts exactly two values: C<global> (the default) and C<us>. There is B<no>
EU inference-geo on the first-party API; a value such as C<eu> is not part of
the enum and is rejected. It is only honoured on Claude 4.6+ models; older
models return a 400 regardless of the value.

    my $claude = Langertha::Engine::Anthropic->new(
        api_key       => $ENV{ANTHROPIC_API_KEY},
        inference_geo => 'us',
    );

The response reports where the request actually ran via
C<usage.inference_geo>, and C<us> residency is billed at 1.1x the base rate.

EU data residency is not available this way. For EU-hosted inference use one of
the EU engines Langertha already ships — L<Langertha::Engine::AKI>,
L<Langertha::Engine::Mistral>, L<Langertha::Engine::Scaleway>,
L<Langertha::Engine::TSystems> or L<Langertha::Engine::Hetzner> — or reach
Claude through the regional endpoints of Amazon Bedrock or Google Vertex AI,
where C<inference_geo> does not apply.

=head2 update_request

    $self->update_request($http_request);

Adds the C<x-api-key>, C<content-type: application/json>, and
C<anthropic-version> headers to outgoing requests.

=head2 chat_request

    my $request = $engine->chat_request($messages, %extra);

Generates an Anthropic-format message request (C<POST /v1/messages>).
Includes model, messages, max_tokens, temperature, reasoning-effort and
prompt-cache controls, and optional C<system>. Returns an HTTP request
object.

=head2 _native_structured_output

Internal predicate. True when the engine's wire supports native structured
output via C<output_config.format> (the first-party Claude Messages API); false
(the default) for the legacy C</anthropic> shim engines, which fall back to the
ADR 0005 synthesized-tool rewrite. L<Langertha::Engine::Anthropic> overrides it
to a true value. It describes the B<endpoint> and so decides the manifest
dialect (C<anthropic> vs C<anthropic-compat>, L<Langertha::Manifest::Builder>);
the request builders ask L</_native_structured_output_for_model> instead.

=head2 _native_structured_output_for_model

Internal predicate the request builders use to pick the structured-output path
for the current C<chat_model>: native C<output_config.format> when true, the
ADR 0005 synthesized-tool rewrite when false. Defaults to
L</_native_structured_output>; L<Langertha::Engine::MoonshotAnthropic>
overrides it to be true on C<kimi-k3> only.

=head2 _translate_response_format

Internal: turns a C<response_format> hash into a synthesized tool plus a
C<tool_choice>, returning the synthetic tool name. Returns C<undef> when no
usable structure is present. Used by the legacy C</anthropic> shim engines and,
for a bare C<json_object>, by first-party L<Langertha::Engine::Anthropic> (which
routes a C<json_schema> natively via C<output_config.format> instead). The
synthesized tool is forced via a named C<tool_choice> where the model supports
forced tool use, and degraded to C<tool_choice> C<auto> where it does not
(C<claude-fable-5-1> / C<claude-mythos-5-1>).

=head2 _normalize_tool_params

Internal: normalizes C<tool_choice> to Anthropic's native format and folds
C<parallel_tool_use> into the C<tool_choice> block as C<disable_parallel_tool_use>.

=head2 chat_response

    my $response = $role->chat_response($http_response, $rf_routed);

Parses an Anthropic-format message response into a L<Langertha::Response>
object. When C<$rf_routed> (a synthetic tool name, or truthy for the
attribute path) and tool calls are present, lifts the first tool_use
arguments back into C<content> as JSON.

A 200 body that is an error envelope (C<{"type":"error","error":{...}}>), or
carries an C<error> object and no content, croaks with
C<< <engine class> response carried an error: MESSAGE >>, as the
OpenAI-compatible parser does.

=head2 _usage_input_includes_cache



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