NAME
Langertha::Moment - Instant on the wire — a Time::Moment that numifies to its Unix epoch
VERSION
version 0.503
SYNOPSIS
my $m = Langertha::Moment->from_wire('2026-05-05T02:53:03.138043625Z');
print 0 + $m; # 1777949583 — the Unix epoch
print "$m"; # 2026-05-05T02:53:03.138043625Z
print $m->nanosecond;# 138043625
print $m->year; # 2026 — every Time::Moment method is available
my $o = Langertha::Moment->from_wire(1777949583); # OpenAI-shaped epoch
my $x = Langertha::Moment->from_wire('garbage'); # undef, never dies
DESCRIPTION
Canonical value object for an instant reported by a provider — currently "created" in Langertha::Response.
Langertha::Moment is a subclass of Time::Moment and inherits its whole API: nanosecond resolution, UTC offsets, comparison, arithmetic. What it adds is an overload set that makes the object drop into the places a plain Unix timestamp used to sit:
0+yields "epoch" in Time::Moment — whole seconds since the epoch. This is the compatibility contract. Every provider on the OpenAI-compatible wire reportscreatedas an epoch integer, and that is the number "to_hash" in Langertha::Response keeps emitting.""yields "to_string" in Time::Moment — the full ISO-8601 stamp, sub-seconds included. Nothing is rounded away on the way in, so an Ollama stamp round-trips byte for byte.<=>compares instants. Against another Time::Moment it delegates to "compare" in Time::Moment and is nanosecond-exact; against a plain number it compares whole epoch seconds. The inherited overload woulddieon the second case ("can only be compared to another Time::Moment object"), which is precisely what code written against the oldIntdoes.cmpcompares the ISO-8601 strings.boolis a constant true — an instant that exists is true, including the epoch-0 one. Note this differs from the plainIntthe attribute used to hold, where0was false; "has_created" in Langertha::Response is the predicate for "did the provider report a stamp at all", and it is unaffected.
Why a subclass rather than an attribute pair
The alternative — keep an Int and hang a second "and here are the nanoseconds" field beside it — is the shape ADR 0011 already rejected once for timing. One value, one object, and the provider's native form stays reachable under "raw" in Langertha::Response.
JSON
TO_JSON returns the epoch number, so an encoder with convert_blessed enabled serializes a moment exactly where an Int used to sit — inside "to_hash" in Langertha::Response and anywhere else a caller drops the value into a structure of their own.
This overrides Time::Moment's own TO_JSON, which returns the ISO-8601 string. Inheriting it would have been the quieter bug of the two: no error, just a field that silently changed from number to string in every trace and log that carries a response. The number is the compatibility surface; the string is one "$moment" away.
An encoder without convert_blessed still dies on the object, as it does on every other value object a Langertha::Response carries (Langertha::Usage, Langertha::ToolCall, Langertha::RateLimit). That is a real break against the old plain Int — see "Comparing and printing created" in Langertha::Response.
TO_JSON
Returns the Unix epoch as a number, so a moment encodes as the plain integer the field held before this class existed. Overrides Time::Moment's TO_JSON, which returns the ISO-8601 string.
from_wire
my $m = Langertha::Moment->from_wire( $value ); # or undef
The lenient inbound constructor: the one entry point that turns whatever a provider put in a timestamp field into a Langertha::Moment, and returns undef — never dies — when it cannot.
Accepts, in this order:
an existing
Langertha::Moment(returned unchanged), or any other Time::Moment, re-parsed from its canonical string;an epoch number, integer or fractional, as a number or as a digit string — the OpenAI-compatible wire form; a 13-digit value is read as a millisecond epoch and scaled to seconds, keeping its sub-second precision, since no representable seconds epoch reaches that magnitude;
an ISO-8601 / RFC3339 string, parsed with "from_string" in Time::Moment in
lenientmode, keeping sub-seconds at full precision.
Returns undef for anything else, for a value outside Time::Moment's representable range, and for a stamp in year 1..999 — the band Go-based servers use for their "no timestamp" zero value.
Leniency is the point. A timestamp is metadata; it must never be able to take a whole provider response down with it, which is exactly what happened when "created" in Langertha::Response was a Maybe[Int] and Ollama sent a string (GitHub issue #3, karr #92 / #117). Callers that want a parse failure to be loud should use the inherited "from_string" in Time::Moment or "from_epoch" in Time::Moment directly — those still die.
SEE ALSO
Time::Moment - The parent class; its whole API is available here
Langertha::Response - Carries a
Langertha::Momentascreated
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.