Langertha

 view release on metacpan or  search on metacpan

docs/superpowers/plans/2026-08-14-symmetric-anthropic-wire-role.md  view on Meta::CPAN

  Anthropic wire envelope, moved verbatim into the role (no signature changes).

**Move list** (from `AnthropicBase` → `Role::AnthropicCompatible`), keeping code byte-identical
except for the package name and the `use Moose::Role;` swap:

- `use` statements needed by moved code: `Carp qw(croak)`, `JSON::MaybeXS`,
  `Langertha::ToolChoice`, `Langertha::Tool`, `Langertha::Response`, `Langertha::ToolCall`.
- Attributes: `api_key` (+ `_build_api_key`), `api_version` (+ `_build_api_version`), `effort`,
  `inference_geo`.
- Envelope defaults: `content_format => 'anthropic'`, `default_response_size` (1024).
- Auth: `update_request` (x-api-key / content-type / anthropic-version headers).
- Chat: `chat_request`, `chat_response`, `Streaming`: `stream_format`, `chat_stream_request`,
  `parse_stream_chunk`.
- Structured-output emulation: `$SYNTH_RF_TOOL_NAME`, `_translate_response_format`,
  `_normalize_tool_params`.
- Wire-format default: `_build_tool_wire_format => 'anthropic'`, `_build_reasoning_wire_format
  => 'anthropic'`, `_build_cache_wire_format => 'anthropic'`.
- Models listing: `list_models_request`, `list_models_response`, `_fetch_all_models`,
  `list_models`.
- Rate limits: `_parse_rate_limit_headers`.

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

  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;

lib/Langertha/HTTP/Redirect.pm  view on Meta::CPAN

use warnings;
use HTTP::Request;
use URI;
use URI::Escape qw( uri_unescape );


# Headers that describe the representation, not the caller: the only ones that
# go to another origin.
my %KEEP = map { $_ => 1 } qw(
  accept accept-charset accept-encoding accept-language
  content-type content-language user-agent
);

# Names under which a request carries a credential. Only used to find the
# secret values the chain sent (which must not reach another origin in a URL);
# headers to another origin are dropped by the %KEEP allowlist, not by name.
# Anchored on the whole name: a pagination cursor (Gemini's pageToken,
# page_token, nextPageToken) is not a credential, and treating its value as one
# would drop it from a cross-origin redirect and lose the page.
my $CREDENTIAL_NAME = qr/\A(?:
    (?:api[-_]?)?key

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

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} // {};

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

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.

t/10_engine_hierarchy.t  view on Meta::CPAN

ok(Langertha::Engine::Anthropic->does('Langertha::Role::Streaming'), 'Anthropic does Streaming');
ok(Langertha::Engine::Anthropic->does('Langertha::Role::Tools'), 'Anthropic does Tools');
ok(!Langertha::Engine::Anthropic->does('Langertha::Role::OpenAICompatible'), 'Anthropic does NOT OpenAICompatible');

{
  my $a = Langertha::Engine::Anthropic->new(api_key => 'test-key');
  is($a->url, 'https://api.anthropic.com', 'Anthropic url defaults correctly');
  is($a->default_model, 'claude-sonnet-5', 'Anthropic default_model');
  my $req = $a->chat('hello');
  is($req->header('x-api-key'), 'test-key', 'Anthropic uses x-api-key header');
  is($req->header('content-type'), 'application/json', 'Anthropic sets content-type');
  like($req->uri, qr{/v1/messages$}, 'Anthropic chat endpoint');
}

# --- Gemini ---

use Langertha::Engine::Gemini;

ok(Langertha::Engine::Gemini->isa('Langertha::Engine::Remote'), 'Gemini isa Remote');
ok(!Langertha::Engine::Gemini->isa('Langertha::Engine::OpenAIBase'), 'Gemini is NOT OpenAIBase');
ok(Langertha::Engine::Gemini->does('Langertha::Role::Chat'), 'Gemini does Chat');

t/22_mistral_transcription.t  view on Meta::CPAN

# timestamp_granularities through, and send the list-valued ones as one part
# per element: an ArrayRef under the plain spec name would otherwise be taken
# for a file spec and its first element opened as a path (k286). Those parts
# carry the PLAIN name, no [] (k315): Mistral's SDKs (Speakeasy multipart
# "standard") and its curl docs send repeated timestamp_granularities /
# context_bias parts, and a name[] key risks being silently dropped.
#
# Fixture provenance (k310): mistral_transcription_capture.json and
# mistral_transcription_segments_capture.json are verbatim captures (Mistral
# voxtral-mini-latest, 2026-09-30, a 6 s English ogg; the second with
# timestamp_granularities=segment; headers files keep only content-type).
# mistral_transcription_doc.json (the 200 example of Mistral's API reference,
# docs.mistral.ai/api/endpoint/audio/transcriptions, also in share/mistral.yaml,
# long text shortened) and mistral_transcription_segments_doc.json (built from
# the TranscriptionSegmentChunk schema of the same spec) stay documentation-
# derived: the capture has speaker_id null and no diarized speakers.

my $boundary = 'XyXLaXyXngXyXerXyXthXyXaXyX';
my $data_dir = path(__FILE__)->parent->child('data');
my $file     = $data_dir->child('testfile')->absolute;
my $json     = JSON::MaybeXS->new->canonical(1)->utf8(1);

t/46_gemini_endpoint_seam.t  view on Meta::CPAN

  is( $request->header('authorization'), 'Bearer bearer_token_456',
    'the credential can leave the query string for a header' );

  is( "" . $shim->chat_stream('hi')->uri,
    "$shim_base/publishers/google/models/gemini-2.0-flash:streamGenerateContent?alt=sse",
    'streaming keeps alt=sse when the auth query is empty' );

  is( "" . $shim->list_models_request->uri, "$shim_base/models",
    'model listing follows the same seam' );

  is( $request->header('content-type'), 'application/json',
    'the inherited request envelope is untouched' );
}

done_testing;

t/70_transcription_response.t  view on Meta::CPAN

# JSON string", and verbose_json kept only ->{text}, silently dropping the
# segments and word timestamps the caller asked for.
#
# Fixture provenance (k310): the openai_transcription_* fixtures are NOT
# captures -- they follow the response examples of OpenAI's audio API reference
# (platform.openai.com/docs/api-reference/audio); the OpenAI account had no
# credits on 2026-09-30 (HTTP 429 insufficient_quota), so they stay
# documentation-derived, including the assumed srt/vtt Content-Type. The
# groq_transcription_{json,verbose_json,text} fixtures ARE verbatim captures
# (Groq whisper-large-v3-turbo, 2026-09-30, a 6 s English ogg; the headers
# files keep only content-type).
#
# The speaches_transcription_* fixtures are verbatim captures too (k370), from
# speaches -- the successor of faster-whisper-server, the server
# Engine::Whisper is written for -- answering Engine::Whisper's own requests on
# 2026-10-19: speaches 0.8.3 (OpenAPI info.version; source speaches-ai/speaches
# master @ 993994f, faster-whisper 1.1.1), model Systran/faster-whisper-tiny.en,
# audio ex/sample.ogg. Bodies are byte-exact; the headers files keep only
# content-type, as for Groq -- speaches sends nothing else a parser reads (no
# rate-limit or request-id headers; just date, server, content-length).
# speaches_transcription_error_no_model is the 422 body for a request without
# a model field (Engine::Whisper's default), verbose_json_words the answer to
# timestamp_granularities[] = word, segment.

my $data_dir = path(__FILE__)->parent->child('data');
my $json     = JSON::MaybeXS->new->canonical(1)->utf8(1);

sub fixture_http {
  my ( $name, $ext, $code, $message ) = @_;

t/data/aki_chat_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "297",
   "content-type" : "application/json",
   "date" : "Tue, 01 Sep 2026 19:24:09 GMT",
   "server" : "Apache"
}

t/data/akianthropic_chat_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "328",
   "content-type" : "application/json",
   "date" : "Tue, 01 Sep 2026 19:24:09 GMT",
   "server" : "Apache"
}

t/data/akianthropic_tool_call_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "375",
   "content-type" : "application/json",
   "date" : "Tue, 01 Sep 2026 19:24:43 GMT",
   "server" : "Apache"
}

t/data/akiopenai_chat_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "589",
   "content-type" : "application/json",
   "date" : "Tue, 01 Sep 2026 19:24:08 GMT",
   "server" : "Apache"
}

t/data/akiopenai_gemma4_image_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "780",
   "content-type" : "application/json",
   "date" : "Fri, 25 Sep 2026 17:30:12 GMT",
   "server" : "Apache"
}

t/data/akiopenai_gptoss_tool_call_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "802",
   "content-type" : "application/json",
   "date" : "Fri, 25 Sep 2026 07:06:10 GMT",
   "server" : "Apache"
}

t/data/akiopenai_qwen36_image_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "903",
   "content-type" : "application/json",
   "date" : "Fri, 25 Sep 2026 17:24:11 GMT",
   "server" : "Apache"
}

t/data/akiopenai_qwen38_image_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "857",
   "content-type" : "application/json",
   "date" : "Fri, 25 Sep 2026 17:30:17 GMT",
   "server" : "Apache"
}

t/data/akiopenai_tool_call_response.headers.json  view on Meta::CPAN

{
   "access-control-allow-origin" : "*",
   "connection" : "close",
   "content-length" : "682",
   "content-type" : "application/json",
   "date" : "Tue, 01 Sep 2026 19:23:44 GMT",
   "server" : "Apache"
}

t/data/anthropic_chat_response.headers.json  view on Meta::CPAN

   "anthropic-ratelimit-requests-remaining" : "9999",
   "anthropic-ratelimit-requests-reset" : "2026-09-17T18:21:21Z",
   "anthropic-ratelimit-tokens-limit" : "12000000",
   "anthropic-ratelimit-tokens-remaining" : "12000000",
   "anthropic-ratelimit-tokens-reset" : "2026-09-17T18:21:21Z",
   "anthropic-workspace-id" : "wrkspc_00000000000000000000000000",
   "cf-cache-status" : "DYNAMIC",
   "cf-ray" : "a3ca214d79d9c809-DUS",
   "connection" : "close",
   "content-security-policy" : "default-src 'none'; frame-ancestors 'none'",
   "content-type" : "application/json",
   "date" : "Thu, 17 Sep 2026 18:21:21 GMT",
   "request-id" : "req_011Cf9UYR7jj39phdTafTyV4",
   "server" : "cloudflare",
   "server-timing" : "x-originResponse;dur=402",
   "strict-transport-security" : "max-age=31536000; includeSubDomains; preload",
   "traceresponse" : "00-7c7d053ca5b57a825de9471199e07d6a-0686456cd2f6f07a-01",
   "vary" : "Accept-Encoding",
   "x-robots-tag" : "none"
}

t/data/groq_transcription_json.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/groq_transcription_text.headers.json  view on Meta::CPAN

{
   "content-type" : "text/plain; charset=UTF-8"
}

t/data/groq_transcription_verbose_json.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/mistral_transcription_capture.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/mistral_transcription_segments_capture.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/openai_chat_response.headers.json  view on Meta::CPAN

{
   "access-control-expose-headers" : "CF-Ray",
   "alt-svc" : "h3=\":443\"; ma=86400",
   "cf-cache-status" : "DYNAMIC",
   "cf-ray" : "a3ca1eeb89c6c185-DUS",
   "connection" : "close",
   "content-type" : "application/json",
   "date" : "Thu, 17 Sep 2026 18:19:45 GMT",
   "openai-organization" : "org-redacted",
   "openai-processing-ms" : "523",
   "openai-project" : "proj_redacted",
   "openai-version" : "2020-10-01",
   "server" : "cloudflare",
   "strict-transport-security" : "max-age=31536000; includeSubDomains; preload",
   "x-content-type-options" : "nosniff",
   "x-openai-proxy-wasm" : "v0.1",
   "x-ratelimit-limit-requests" : "5000",
   "x-ratelimit-limit-tokens" : "4000000",
   "x-ratelimit-remaining-requests" : "4999",
   "x-ratelimit-remaining-tokens" : "3999996",
   "x-ratelimit-reset-requests" : "12ms",
   "x-ratelimit-reset-tokens" : "0s",
   "x-request-id" : "req_13322fa5760e4044b0a3f24ee8dfb3f7"
}

t/data/openai_compatible_image_url.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/openai_image_gpt_image_b64.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/openai_transcription_json.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}

t/data/openai_transcription_srt.headers.json  view on Meta::CPAN

{
   "content-type" : "text/plain; charset=utf-8"
}

t/data/openai_transcription_text.headers.json  view on Meta::CPAN

{
   "content-type" : "text/plain; charset=utf-8"
}

t/data/openai_transcription_verbose_json.headers.json  view on Meta::CPAN

{
   "content-type" : "application/json"
}



( run in 2.798 seconds using v1.01-cache-2.11-cpan-99fa8243a42 )