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.