NAME
Langertha::Skeid::Protocol::Documents - The document conversion routes of docling-serve -- relayed, the pages read off the answer
VERSION
version 0.004
DESCRIPTION
Serves POST /v1/convert/file and POST /v1/convert/source, the synchronous calls of docling-serve, under docling's own paths so that a docling client only changes its base URL. They are relayed routes (ADR 0021, ADR 0028): the node's endpoint of the same name gets the request in the shape the client sent it, and its answer -- JSON, or a zip when several documents or outputs come back -- goes back untouched. This module translates nothing. It holds what Skeid has to know about that wire all the same, and it is the only place that knows it: the routes, the request fields that make a node fetch from a URL, where an answer says how many pages it was, and the two node options (kind, api_key_header) the route family brings.
A docling request has no model. The route is planned over the nodes of kind documents, under the routing key "routing_key", which a key's policy grants like a model name (ADR 0008, 0028).
Everything here is a class method.
routes
my @paths = Langertha::Skeid::Protocol::Documents->routes;
The client paths of the documents routes, sorted: /v1/convert/file and /v1/convert/source.
upstream_path
The path below a node's URL a client path is relayed to (/convert/file), or undef for a path that is not a documents route.
api_format
The api_format of a documents event: docling.
routing_key
The name the route is planned, authorised and reported under: documents. See "DESCRIPTION".
kinds
my @kinds = Langertha::Skeid::Protocol::Documents->kinds; # ('documents')
The values a node's kind can take besides the default chat, sorted.
normalize_kind
my $kind = Langertha::Skeid::Protocol::Documents->normalize_kind($node{kind});
A node's kind as it is kept: lowercased and trimmed; the empty string for none or chat, which is a chat node (the default, and what every node was before the option). Croaks on any other value than "kinds", so a misspelled kind fails the config load instead of leaving a documents node to receive chat requests, or the reverse.
normalize_key_header
my $header = Langertha::Skeid::Protocol::Documents->normalize_key_header($node{api_key_header});
The header a node wants its key in, as it is kept: the empty string for none -- the key goes in Authorization: Bearer, as for every node -- also for Authorization itself, the one header that is spelled with Bearer. Any other name is the header the raw key is sent in (X-Api-Key for docling-serve). Croaks unless the name is a token of RFC 7230, so no value can carry a line break into the upstream request.
max_parse_bytes
The largest answer, in bytes, Skeid decodes to read pages off it (8 MiB). The proxy passes it to the relay, which leaves a larger answer undecoded.
form_remote_fields
my ($names, $problem) = Langertha::Skeid::Protocol::Documents->form_remote_fields($req->content);
The remote-source fields of a multipart form (callbacks, http_sources, and sources, target, which carry addresses too), by the name the client spelled them -- each once. The part names are read as Audio's form_fields reads them, liberally, and a part that carries a file name counts as well: a node's parser may take it for the field. $problem is a reason to refuse the whole form (a nested multipart, a name in the name* notation), else undef.
json_remote_fields
my $names = Langertha::Skeid::Protocol::Documents->json_remote_fields($decoded, $bytes);
The remote-source fields of a decoded /v1/convert/source body: http_sources and callbacks when present and not empty, sources when an entry is anything but a file source (kind: file, or an inline base64_string), target when it is not a kind inbody or zip -- an object naming an address to put the result to. Also, by the raw $bytes, a field the body names more than once: two parsers may pick different ones, and the one Skeid reads must not hide the one the node reads. Returns the names, each once, sorted.
document_count
my $n = Langertha::Skeid::Protocol::Documents->document_count($decoded); # a source body
my $n = Langertha::Skeid::Protocol::Documents->document_count($content); # a parsed form
How many input documents a request carries: the file parts of a form, or the entries of a source body's file_sources, http_sources and sources.
pages
my $pages = Langertha::Skeid::Protocol::Documents->pages($payload, $res);
The page count of a converted document, off the decoded answer, or undef when the answer does not say. It is read from document.json_content.pages -- the DoclingDocument keeps one entry per page, keyed by page number -- and only from a successful JSON answer: not from a zip, which is not decoded at all, and not from an answer past "max_parse_bytes", which the relay did not decode. json_content may be the object or, in some versions, a string holding it. A document format without pages (pages empty or absent) says nothing: no pages rather than a zero nobody measured.
The cost is one decode of the answer, which the relay does for every JSON answer anyway, bounded by "max_parse_bytes"; nothing is decoded a second time except a json_content that is a string.
SEE ALSO
Langertha::Skeid::Proxy, Langertha::Skeid::Protocol::Audio, ADR 0021, ADR 0028
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.