NAME

Langertha::Manifest::Builder - Build a provider manifest from configured engines (offline, never copies a secret)

VERSION

version 0.503

SYNOPSIS

use Langertha::Manifest::Builder;

# One engine, one endpoint
my $manifest = Langertha::Manifest::Builder->from_engine(
  Langertha::Engine::vLLM->new( url => 'http://gpu01:8000/v1', model => 'qwen3' ),
);
print $manifest->to_json;

# A proxy exposing several protocol endpoints under its public URL
my $builder = Langertha::Manifest::Builder->new(
  provider_id => 'my-knarr',
  issuer      => 'https://knarr.example',
);
$builder->add_engine( $openrouter_engine,        # no model of its own needed
  endpoint_id => 'openai', base_url => 'https://knarr.example/v1',
  models      => [ 'gpt-5.6', 'local-qwen' ] );
$builder->add_endpoint( id => 'ollama', dialect => 'ollama',
  base_url => 'https://knarr.example' );
$builder->add_model( id => 'local-qwen', endpoint_ref => 'ollama',
  capabilities => { chat => 1, streaming => 1 } );
my $manifest = $builder->manifest;

DESCRIPTION

Maps configured Langertha chat engines into a Langertha::Manifest. Everything is read from the engine object: no network I/O happens (in particular list_models is never called), and the caller's engine is not touched — lazy attributes (model, chat_model, api_key) are evaluated on in-memory clones, never on the engine itself.

  • dialect — from the engine family, most specific class first: Langertha::Engine::Perplexity → perplexity-agent, Langertha::Engine::OpenAIResponses → responses, Langertha::Engine::OpenAIBase → openai-chat, Langertha::Engine::AnthropicBase → anthropic when the engine emits first-party native structured output (output_config.format), otherwise anthropic-compat (the /anthropic shims: AKIAnthropic, MiniMaxAnthropic, MoonshotAnthropic, LMStudioAnthropic), Langertha::Engine::Gemini → gemini, Langertha::Engine::Ollama → ollama, Langertha::Engine::AKI → aki, Langertha::Engine::LMStudio → lmstudio. An engine that is not a chat engine (e.g. the transcription-only Langertha::Engine::Whisper) croaks.

  • base_url — the engine's url (override with base_url to publish a public URL instead of an internal one).

  • auth — from the engine class's api_key_required / api_key_env: a required key yields an api_key auth entry; an optional key only when one is configured; no key, no entry. Only the definedness of the key is looked at — its value never enters the manifest.

  • models — models => [...] when given. Otherwise the engine's configured model; a placeholder id (default, which the self-hosted engines use for "whatever the server loaded", or an empty id) is skipped rather than published (with a warning, since the endpoint then lists no models), and an engine with no model at all croaks asking for models.

  • capabilities — engine_capabilities evaluated per model (on a clone with chat_model set to that model id, so model-scoped corrections apply), then filtered to "model_capabilities": only flags that describe a chat call to that model at that endpoint. Names are exactly the registry's (Langertha::Role::Capabilities); the Builder adds none.

Known v1 limitations: engine-class facts that the capability flags cannot express are not in the manifest — the Groq/Cerebras refusal of tools plus response_format in one request (ADR 0024) and OpenAI's temperature gate under active reasoning (ADR 0025).

provider_id

The manifest's provider_id. When not given, the first "add_engine" derives it from the engine class (Langertha::Engine::vLLM → vllm).

issuer

The manifest's issuer. When not given, the first "add_engine" derives it from the origin of the engine's URL.

extensions

HashRef passed into the manifest's extensions (deep-copied there).

model_capabilities

my @names = Langertha::Manifest::Builder->model_capabilities;

The allowlist of capability names a Builder-made model entry may claim — the flags that describe a chat call to that model: chat, streaming, the tool flags (tools_native, tools_hermes, tool_choice_auto, tool_choice_any, tool_choice_none, tool_choice_named, parallel_tool_use), structured output (response_format_json_object, response_format_json_schema), reasoning (reasoning_effort, thinking_budget), sampling and request controls (temperature, seed, system_prompt, response_size) and the request-side prompt-cache controls (prompt_cache, prompt_cache_key), server_tools (the wire accepts provider-native server-side tools; which types is not published), and image_input (the model sees an image part; model-scoped, see Langertha::Role::ImageInput).

Engine-level and client-side flags are never published on a model: embedding, transcription, image_generation, runtime_metrics, prefix_caching, keep_alive, cached_content, context_size (Ollama's server-side num_ctx allocation, like keep_alive).

This filters only what the Builder emits. A parsed manifest accepts any capability name ("supports" in Langertha::Manifest::Model).

dialect_for_engine

my $dialect = Langertha::Manifest::Builder->dialect_for_engine($engine);

The manifest dialect of an engine (see "DESCRIPTION"), or undef when its family has none.

engine_class_for_dialect

my $engine_class = Langertha::Manifest::Builder->engine_class_for_dialect('openai-chat');
my $engine = $engine_class->new( url => $endpoint->base_url, api_key => $key );

The inverse of "dialect_for_engine": the generic engine class that speaks a manifest dialect, loaded and ready for new, or undef for an unknown (or undefined) dialect. It never croaks on an unknown dialect.

Each dialect maps to the one class that takes the endpoint's base_url as its url, not to a vendor subclass: openai-chat is Langertha::Engine::OpenAI (the ~25 OpenAI-compatible engines share that wire), responses Langertha::Engine::OpenAIResponses, perplexity-agent Langertha::Engine::Perplexity, anthropic Langertha::Engine::Anthropic, anthropic-compat Langertha::Engine::AnthropicBase (the /anthropic shims), gemini Langertha::Engine::Gemini, ollama Langertha::Engine::Ollama, aki Langertha::Engine::AKI and lmstudio Langertha::Engine::LMStudio. Every dialect of "known_dialects" in Langertha::Manifest::Endpoint has a class, and a test holds the round trip: dialect_for_engine of that class gives the dialect back.

from_engine

my $manifest = Langertha::Manifest::Builder->from_engine( $engine, %options );

Shortcut: a builder with provider_id / issuer / extensions from %options, one "add_engine" with the rest, then "manifest".

add_engine

$builder->add_engine( $engine,
  endpoint_id => 'chat',            # default 'chat'
  base_url    => $public_url,       # default $engine->url
  models      => [ ... ],           # default: the engine's model (placeholders skipped)
  auth        => 'api_key',         # or 'none'; default from the engine class
  auth_id     => 'api',             # default 'api' (shared across engines)
  dialect     => 'openai-chat',     # default from the engine family
);

Adds one endpoint for the chat engine, its auth entry (if any) and one model entry per model id. Croaks for a non-chat engine, for a chat engine without a manifest dialect unless dialect is given, for a model-less engine unless models is given, and on a duplicate endpoint id or (model, endpoint) pair. Atomic: on a croak the builder is unchanged. Returns the builder.

add_endpoint

$builder->add_endpoint( id => ..., dialect => ..., base_url => ..., auth_ref => ... );

Adds an endpoint that is not an engine (a proxy's own protocol route). Croaks on a duplicate id.

add_auth

$builder->add_auth( id => 'api', type => 'api_key' );

Adds an auth entry. Croaks on a duplicate id.

add_model

$builder->add_model( id => ..., endpoint_ref => ..., capabilities => { ... } );

Adds a model entry as given (no capability filtering: the caller states the claim). Croaks on a duplicate (id, endpoint_ref) pair.

manifest

Returns the validated Langertha::Manifest.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.