NAME

Langertha::ToolChoice - Immutable canonical tool-selection policy with cross-provider conversion

VERSION

version 0.503

SYNOPSIS

use Langertha::ToolChoice;

my $choice = Langertha::ToolChoice->specific('get_weather');

$choice->to('openai');     # { type => 'function', function => { name => 'get_weather' } }
$choice->to('anthropic');  # { type => 'tool', name => 'get_weather' }
$choice->to('responses');  # { type => 'function', name => 'get_weather' }
$choice->to('gemini');
# { functionCallingConfig => { mode => 'ANY',
#                              allowed_function_names => ['get_weather'] } }

# Normalize whatever the caller passed (string, any provider's hash,
# or a ToolChoice object) into the canonical form
my $tc = Langertha::ToolChoice->from_hash('required');   # type 'any'
$tc    = Langertha::ToolChoice->from_hash(
    { type => 'function', function => { name => 'extract' } } );
say $tc->type, ' ', $tc->name;   # tool extract

# A ToolChoice object is valid tool_choice input on every engine
my $response = await $engine->chat_f(
    messages    => [ { role => 'user', content => $prompt } ],
    tools       => [ $tool ],
    tool_choice => Langertha::ToolChoice->specific('extract'),
);

DESCRIPTION

Canonical value object for the tool-selection policy of a request: may the model call a tool, must it, and must it call one particular tool. It sits beside Langertha::Tool, Langertha::ToolCall and Langertha::ToolResult in the tool wire-translation seam: request builders normalize the caller's tool_choice with "from_hash" and serialize it with "to", dispatched by the engine's tool_wire_format, so the per-provider spelling lives in this one place (ADR 0001, ADR 0010).

The canonical vocabulary has four policies, held in "type":

  • auto - the model decides whether to call a tool

  • any - the model must call some tool (OpenAI's required)

  • none - the model must not call a tool

  • tool - the model must call the tool named in "name"

A ToolChoice object is valid tool_choice input on every engine and tool wire: "from_hash" hands an object back unchanged, and every request builder serializes it through "to" rather than passing the object through. What happens beyond the serialized value (for example "chat_f" in Langertha::Role::Chat rewriting a named choice into a response_format on engines without the tool_choice_named capability) is the engine's business, not this class's.

Instances are immutable.

type

Required. The canonical policy: one of auto, any, none or tool (the Langertha::ToolChoice::Type enum). Anything else fails the type constraint at construction.

name

The tool to force when "type" is tool; undef by default. A tool choice without a non-empty name serializes as auto on every wire. The name is not checked against the request's tool list.

auto

my $auto = Langertha::ToolChoice->auto;

Class method that builds an auto choice.

any

my $any = Langertha::ToolChoice->any;

Class method that builds an any choice.

none

my $none = Langertha::ToolChoice->none;

Class method that builds a none choice.

specific

my $choice = Langertha::ToolChoice->specific('get_weather');

Class method that builds a tool choice forcing the named tool.

from_hash

my $choice = Langertha::ToolChoice->from_hash($tool_choice);

Class method that normalizes a caller-supplied tool_choice into a ToolChoice, whichever provider's spelling it uses. Accepts:

  • a ToolChoice object, returned unchanged

  • the strings auto, none, and required or any (both give any)

  • a hash with type auto, none, any or required

  • a named-tool hash in the OpenAI Chat Completions shape { type => 'function', function => { name => ... } }, the flat Responses shape { type => 'function', name => ... }, or the Anthropic shape { type => 'tool', name => ... }; a missing or empty name gives auto

Returns undef for undef and for anything it does not recognize (an unknown string or type, a non-hash reference). It never dies, so callers test the result.

from_openai

Alias for "from_hash", which reads every supported shape regardless of the provider it came from.

from_anthropic

Alias for "from_hash", like "from_openai".

to_openai

my $wire = $choice->to_openai;

Serializes for the OpenAI Chat Completions tool_choice field: the strings auto, none and required (for any), or { type => 'function', function => { name => $name } } for a named tool. The openai entry of "to".

to_anthropic

my $wire = $choice->to_anthropic;

Serializes for the Anthropic Messages tool_choice field: always a hash, { type => 'auto' }, { type => 'any' }, { type => 'none' } or { type => 'tool', name => $name }. The anthropic entry of "to". disable_parallel_tool_use is not part of this value; the request builder in Langertha::Role::AnthropicCompatible folds the engine's parallel_tool_use (Langertha::Role::ParallelToolUse) into the block.

to_perplexity

my $wire = $choice->to_perplexity;   # 'none' | 'auto' | 'required'

Legacy. Serializes to the string forms of Perplexity's old Sonar /chat/completions endpoint: none, auto, and required for both any and a named tool (that wire could not force a named tool). Kept for callers of that endpoint; Langertha::Engine::Perplexity no longer uses it. The Agent API (/v1/agent) that engine speaks has no tool_choice field at all, so the engine never sends one (see Langertha::Role::ResponsesCompatible). Not on the to($fmt) dispatch.

to_gemini

my $wire = $choice->to_gemini;
# { functionCallingConfig => { mode => 'ANY', allowed_function_names => ['x'] } }

Serializes for Gemini's toolConfig: { functionCallingConfig => { mode => 'AUTO' | 'ANY' | 'NONE' } }, and for a named tool mode ANY with allowed_function_names holding that one name. Gemini's VALIDATED mode has no canonical equivalent and is never produced. The gemini entry of "to".

to_responses

my $wire = $choice->to_responses;

Serializes for the Open-Responses tool_choice field (Langertha::Engine::OpenAIResponses): the strings auto, none and required (for any), or the flat { type => 'function', name => $name } for a named tool, without the nested function wrapper of Chat Completions. The responses entry of "to".

to_hash

my $hash = $choice->to_hash;   # { type => 'tool', name => 'extract' }

The canonical, provider-neutral form: type, plus name when it is defined. "from_hash" reads it back.

TO_JSON

Returns "to_hash", so a JSON encoder with convert_blessed enabled serializes the object in its canonical form. That is for logging and tracing (for example Langertha::Plugin::Langfuse); a request body gets the wire form from "to".

to

my $wire = $choice->to( $engine->tool_wire_format );

Serializes for a tool_wire_format by dispatching to "to_openai", "to_anthropic", "to_gemini" or "to_responses". Only those four wires carry a tool_choice request field. ollama and hermes have none, so to croaks for them, as it does for any unknown or undefined format. (On the hermes wire Langertha::Role::Chat handles the choice itself: none withholds the tools from the prompt, anything else but auto is ignored with a warning.) "to_perplexity" is not on this dispatch.

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.