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"
}