NAME
Langertha::Skeid::Protocol - Shared helpers for Skeid wire-format translation
VERSION
version 0.003
DESCRIPTION
Skeid speaks several client dialects but makes exactly one kind of upstream call: an OpenAI-shaped POST to the selected node. Every other API format is translated in on the way up and out on the way back, by a module under this namespace — one per format.
That is the whole rule, and it is load-bearing: a format-specific field name belongs inside its own translator and nowhere else. Routing, admission, usage accounting and the upstream request builder never learn that Anthropic calls it system or that Ollama calls it prompt_eval_count. See docs/adr/0001-one-upstream-call-shape-all-client-formats-translated.md.
This module itself holds only the handful of helpers the translators share. They are plain functions, called fully qualified (Langertha::Skeid::Protocol::utf8_length($text)) and not exported; only "openai_manifest_endpoint" is a class method.
iso8601_now
my $now = Langertha::Skeid::Protocol::iso8601_now(); # '2026-09-29T16:54:00Z'
Current UTC time as YYYY-MM-DDTHH:MM:SSZ.
encode_json_safe
my $line = Langertha::Skeid::Protocol::encode_json_safe($payload) . "\n";
JSON-encodes a value to UTF-8 bytes, returning '{}' rather than dying on anything unencodable. For a whole wire unit that goes out as-is -- one SSE event or NDJSON line. Never for a string nested inside another JSON document: use "encode_json_text_safe".
encode_json_text_safe
my $arguments = Langertha::Skeid::Protocol::encode_json_text_safe($block->{input});
JSON-encodes a value to a character string, returning '{}' rather than dying. For JSON nested as a string inside a body that is encoded as a whole later -- tool_use.input becoming function.arguments, a structured tool_result becoming a tool message's content. Byte output there would be encoded a second time and every non-ASCII character would reach the model as mojibake. Used where a malformed tool argument must not take the whole request down.
utf8_length
my $bytes = Langertha::Skeid::Protocol::utf8_length($text);
Length of a character string in UTF-8 bytes -- what a content_bytes count means. length on decoded text counts characters and undercounts every non-ASCII answer.
decode_json_safe
my $data = Langertha::Skeid::Protocol::decode_json_safe($bytes); # or undef
Decodes a JSON string of UTF-8 bytes (a raw body or SSE payload), returning undef instead of dying. Not for text that is already characters, such as function.arguments read from a decoded body. A reference is passed through unchanged, so it is safe to call on a value that may already be decoded.
image_media_type
my $mt = Langertha::Skeid::Protocol::image_media_type($base64); # 'image/jpeg'
The media type of a base64-encoded image, read from its magic bytes: PNG, JPEG, GIF and WebP are recognised, anything else is reported as image/png. For a client format that sends raw base64 without saying what it is (Ollama's images), so the data URL the OpenAI upstream needs can name a type.
image_url_part
my $part = Langertha::Skeid::Protocol::image_url_part($url);
my $part = Langertha::Skeid::Protocol::image_url_part(undef, $base64, $media_type);
An OpenAI image_url content part: { type => 'image_url', image_url => { url => ... } }. Given a URL, it is used as is. Given base64 data, it becomes a data: URL with $media_type, or with "image_media_type" when none is given.
openai_manifest_endpoint
my $spec = Langertha::Skeid::Protocol->openai_manifest_endpoint;
# { dialect => 'openai-chat', path => '/v1', capabilities => [ ... ] }
How the OpenAI face (/v1/chat/completions) appears in the provider manifest (skeid #29). That face is the upstream call shape itself: its body goes to the node untranslated, apart from the model name, so it carries every OpenAI-chat request field a model entry may claim. capabilities is the list of those flags; a model is published on this face with the capabilities declared for it, cut down to this list. The translated faces carry their own spec ("manifest_endpoint" in Langertha::Skeid::Protocol::Anthropic, "manifest_endpoint" in Langertha::Skeid::Protocol::Ollama).
SEE ALSO
Langertha::Skeid::Protocol::Anthropic, Langertha::Skeid::Protocol::Anthropic::Stream
Langertha::Skeid::Protocol::Ollama, Langertha::Skeid::Protocol::Ollama::Stream
Langertha::Skeid::Proxy -- the routes that use them
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-skeid/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 <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.