NAME

Langertha::Role::ResponsesCompatible - Role for the Open-Responses wire envelope (input/instructions/output[])

VERSION

version 0.503

SYNOPSIS

# Not used directly - composed by engines speaking the Responses envelope.

package My::Engine;
use Moose;

extends 'Langertha::Engine::Remote';

with map { 'Langertha::Role::'.$_ } qw(
    Models Temperature ReasoningEffort ResponseSize SystemPrompt
    ResponseFormat Streaming Chat ResponsesCompatible
);

__PACKAGE__->meta->make_immutable;

DESCRIPTION

The Open-Responses wire envelope, parallel to Langertha::Role::OpenAICompatible and Langertha::Role::AnthropicCompatible. This is the request/response/stream shape that OpenAI's /v1/responses API and Perplexity's /v1/agent Agent API share:

  • input instead of messages (typed items, system messages lifted out)

  • instructions for the system prompt (top-level, not in input)

  • output[] array discriminated by type (message, reasoning, function_call) instead of choices[]

  • input_tokens/output_tokens instead of prompt_tokens/completion_tokens

  • flat tool objects { type, name, description, parameters } and the responses tool_wire_format / reasoning_wire_format

The role owns the body builder, the output[] response walker, the usage normalization, and the typed-SSE stream parser. It does not own authentication (each consumer supplies its own api_key / update_request, so this role never clobbers an inherited key builder) nor the capability corrections (an engine that opts out of streaming, or narrows the response_format enum, does so on itself).

Divergence hooks

Two consumers speak this envelope from different parents and diverge on a handful of slots; each is an overridable method with the OpenAI-Responses default baked in:

chat_request

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

Builds an Open-Responses request body (input / instructions / model or preset / optional structured-output slot / reasoning / max_output_tokens / temperature) and hands it to "_responses_dispatch". Returns an HTTP request object.

_responses_model_kwargs

Returns the model-selection kwargs for the body. Default emits model => chat_model. Overridden by consumers that select a preset or models[] instead (Perplexity maps its user-facing model ids to presets).

_responses_format_kwargs

Returns the structured-output kwargs for the body from a Chat-Completions-shaped response_format hash. Default is OpenAI's text => { format => ... } (flat json_schema, see "_responses_text_format"). Overridden by consumers whose wire keeps the top-level Chat-Completions response_format shape (Perplexity).

_responses_dispatch

Turns the built body into an HTTP request. Default resolves the endpoint from the OpenAPI spec via "chat_operation_id" (createResponse -> /v1/responses). Overridden by consumers on a non-OpenAPI parent to POST a fixed provider path directly (Perplexity -> /v1/agent).

_normalize_input_item

Shapes one input array item from a normalized chat message. Default passes the { role, content } hash through unchanged. Overridden by consumers that require an explicit item type.

_responses_extra_fields

Returns extra Langertha::Response constructor args pulled from the raw response payload. Default empty. Overridden by consumers that surface provider-specific fields (Perplexity lifts search_results into "citations" in Langertha::Response).

_responses_echo_item

my @items = $engine->_responses_echo_item($output_item);

Shapes one output[] item of the previous reply for the tool-loop echo ("format_tool_results" in Langertha::Role::Tools, responses wire), after a function call nested in a message has been hoisted. Returns the item(s) to send back as input, or an empty list to drop it. Default passes every item through unchanged: OpenAI's /v1/responses takes its own output items as input. Overridden by consumers whose input schema is narrower: Perplexity keeps function_call items, turns an assistant message into { type => 'message', role => 'assistant', content => $text }, and drops every other item (search_results, *_results, mcp_*).

chat_response

my $response = $engine->chat_response($http_response);

Walks the output[] array (message / reasoning / top-level function_call), normalizes usage, maps created_at to "created" in Langertha::Response, and returns a Langertha::Response. Extra provider fields come from "_responses_extra_fields".

Server-side call items (web_search_call, file_search_call, mcp_call, ...) land on "server_tool_calls" in Langertha::Response, never on "tool_calls" in Langertha::Response. The url_citation annotations of the answer are merged with any citations from "_responses_extra_fields" onto "citations" in Langertha::Response (hook entries first, one entry per page; see "_responses_merge_citations"). An output item the client must answer and Langertha cannot (mcp_approval_request, computer_call, custom_tool_call, local_shell_call, apply_patch_call, a client tool_search_call) croaks.

finish_reason is tool_calls when the reply carries function calls, stop for a completed message, and the message status (incomplete) for a truncated one. A reply with no message item whose envelope reports status incomplete with incomplete_details.reason max_output_tokens (or max_tokens) has finish_reason length -- whether it holds only function calls or an empty output because reasoning consumed the whole budget. A function call's arguments that do not decode leave it with {} and "arguments_undecodable" in Langertha::ToolCall set.

A refusal content part of a message becomes "refusal" in Langertha::Response (on a stream, the final chunk's refusal).

A body with an error object and an empty or missing output is not an answer and croaks "<engine> response carried an error: <message> (<code>)".

_responses_merge_citations

my $citations = $engine->_responses_merge_citations( $hook_citations, $annotation_citations );

Merges the citations a consumer's "_responses_extra_fields" returned with the url_citation annotations the walker collected. Hook entries come first, then annotations, in wire order. One page is listed once: the dedup key is the url without utm_* query parameters (OpenAI's ?utm_source=openai), the first entry wins, and a later duplicate only fills in fields the first one lacks. The stored url is never rewritten. Without annotations the hook's list is returned unchanged. Returns undef when there is nothing.

stream_format

my $format = $engine->stream_format;

Returns 'sse'. The Responses/Agent stream is a typed SSE stream. A consumer that does not stream (OpenAI's Responses engine) overrides this to undef and clears the streaming capability.

chat_stream_request

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

Builds a streaming (stream = true>) Open-Responses request. Returns an HTTP request object for streaming execution.

parse_stream_chunk

my $chunk = $engine->parse_stream_chunk($data, $event);

Parses one typed-SSE data payload from a Responses/Agent stream. Returns a Langertha::Stream::Chunk for response.output_text.delta (text) and the terminal response.completed / response.incomplete (final chunk: usage, the prefix-cache read count from usage.input_tokens_details.cached_tokens lifted onto "cached_tokens" in Langertha::Stream::Chunk, any usage.cost carried through the usage hash, and — via "_responses_extra_fields" — any search-augmented citations lifted from the completed output[]), undef for every other typed event.

The final chunk's output[] is read by the same walker as "chat_response": the reply's function calls land on it as "tool_calls" in Langertha::Stream::Chunk (collect them with "aggregate_tool_calls" in Langertha::Role::Chat), a reasoning summary lands on its thinking, and its finish_reason is the one "chat_response" reports for the same response: tool_calls when the reply carries function calls, stop for a completed message, the message status (incomplete) for a truncated one, and length for a reply of only function calls that stopped on max_output_tokens. The incremental function-call events are not assembled, so a call is delivered exactly once. A response.failed or error event croaks with the provider's error code and message, which fails the stream.

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.