Repository navigation
Releases: latere-ai/pkg
Releases · latere-ai/pkg
Release list
v0.94.0
Changed
authkit/oidc:HandleCallbackrequires a verified ID token. A token
response without one now goes to/?auth_error=invalid_id_tokenand sets
no session; before, the session was built from the access token's claims
alone, which nothing verified. OpenID Connect requires an ID token on
every authorization-code exchange that asks foropenid, so a relying
party signing in against Latere's auth service, or any conforming issuer,
sees no difference. One configured withoutopenidin its scopes cannot
sign in until it adds it.authkit/oidc: a session cookie over the 4096 bytes a browser stores for
one cookie is refused instead of written. A browser drops a longer cookie
without an error, which left the person signed out with nothing logged.
At sign-in,HandleCallbackgoes to/?auth_error=session_too_large;
SetSessionreturns an error, and a refresh that would outgrow the limit
keeps the cookie the browser already holds and logs why.
v0.93.0
Added
authkit/oidc:Client.ReadSessionreads the session cookie without
refreshing it and without writing. It answersErrRefreshRequiredwhen
the access token is within the refresh leeway of expiring and the session
holds a refresh token, the same momentSessionFromRequestwould
refresh. A relying party that serves one browser from several replicas
reads its API routes with it and refreshes on one route the page calls
one request at a time, so two requests never spend one refresh token.authkit/oidc: silent sign-in./login?prompt=noneasks the issuer to
sign the person in without a page when they already hold a session
there. When the issuer answerslogin_required(or any other error) to
such a login,HandleCallbackgoes back toreturn_towith no
auth_error, as if nothing was tried.HandleLoginforwardsprompt
only with a value OpenID Connect defines (none,login,consent,
select_account), andFlowState.Silentrecords a silent login.
Fixed
authkit/oidc: a page that loads several routes at once no longer signs
the person out. Each request found the access token about to expire and
refreshed the same rotating refresh token; the issuer took the second
use for replay and revoked the session.SessionFromRequest,BuildMe
andUserFromRequestnow spend a refresh token at most once per
process: concurrent requests share one refresh, and a request that still
carries the spent token within 30 seconds is answered with the refresh
already made. Requests that reach different replicas can still race;
seeReadSession.authkit/oidc: a refresh that fails without the issuer refusing it (no
connection, a timeout, a 5xx) wraps the newErrIssuerUnavailableand
no longer signs the person out:BuildMeandUserFromRequestkeep the
cookie, so the next request tries again. A refresh the issuer refuses
wrapsErrSessionExpired, soerrors.Istells the two apart on the
errorSessionFromRequestreturns.
v0.92.0
Added
vendors/linkup: a client for Linkup's web search API.Searchsends
oneRequest, the query with one of four depths (flash,fast,
standard,deep) and an output type (ranked results, a sourced answer,
or an object that follows a JSON Schema), optionally narrowed to domains,
a date range and a result count, and returns aResponseholding what
that output type answers. A status other than 200 is an*Errorwith the
status, Linkup's error code and message, the refused fields and the
Retry-Afterwait, and it matches exactly one ofErrBadRequest,
ErrNoResult,ErrAuth,ErrInsufficientCredit,ErrRateLimitedand
ErrUpstream, so a rate limit that passes is told apart from exhausted
credit that does not. The API key never appears in an error, also where
Linkup's error body echoes it. Requests go throughotel.HTTPClient
unlessWithHTTPClientreplaces it, and nothing is retried, since every
search is billed.
Changed
typesafeaimoves tovendors/typesafe, the directory that holds one
client per third-party API, with its API unchanged. Import
latere.ai/x/pkg/vendors/typesafe; the package name staystypesafe, so
only the import path changes. The old path is removed, with no forwarding
package.
v0.91.0
Added
verdict: the vocabulary every decision point shares when it decides
whether an automated action runs.Allow,Flag,AskandBlock
in their order;Least, under which a verdict outside the four counts
asBlock, so composition only narrows;OnFailure, which is never
more permissive thanAsk; andDecide, which applies a ceiling and
random review sampling to a suggested verdict and returns the verdict
with the probability, fixed before the action runs, that a person sees
it. Recording that probability with each decision is what lets any
decision source's error rate be estimated without bias. A harness that
ranks verdicts itself should move toLeast: ranking an unknown
verdict by its index in a list makes it the most permissive.
v0.90.2
- OpenTelemetry Go v1.46.0, with its log modules v0.22.0, the slog bridge v0.20.1 and otelhttp v0.71.0, past GO-2026-6615 and GO-2026-6505.
otelnames its resource with semantic conventions v1.43.0, the SDK's own schema since v1.46.0; with v1.41.0 the two schemas conflict and telemetry export is disabled at start.
v0.90.1
Fixed
llmdialect/openaichatreadsnative_finish_reason, the provider's
own finish reason an aggregator such as OpenRouter passes beside the
normalized one. A native reason naming the output limit (length,
max_tokens,max_output_tokens,MAX_TOKENS) ismax_tokens
whateverfinish_reasonsays. OpenRouter reports a response cut at the
limit inside a tool call's arguments astool_calls, so the call
reached the caller as complete with arguments that are not JSON.- The
llmdialect/openaichatstream decoder keeps the IR grammar for
parallel tool calls whose arguments interleave: a call announced while
the open call's arguments are not yet one JSON value waits, buffered,
until they are or the stream ends, and its block then starts with what
it received. It closed a call's block when the next call began, so
arguments that arrived after for the earlier call were args deltas for
a block already stopped, which a consumer that checks the grammar
refuses. Calls that arrive one after another stream as before.
v0.90.0
Fixed
hostsandbox.Driver.Preflightlooks forbwrapon Linux, the program
thebubblewrappackage installs. It looked for an executable named
bubblewrap, which no package provides, so a Linux machine with
Bubblewrap installed was refused. The refusal names the component
bwrap, andDefaultRemedieskeys its rowbwrap, with install lines
that still name thebubblewrappackage. A consumer whoseConfig.Look
mappedbubblewraptobwrapcan drop the mapping; one that adds or
replaces that row throughConfig.Remedies, or calls
Remedies.NotReadywithbubblewrap, namesbwrapinstead.
Changed
hostsandbox.Driver.Stop, andDiscardwith it, also ends the
processes a stage left running in its process group after its main
process exited. It returned at once for a stage with a recorded exit
status, so a consumer that wanted those processes gone had to parse the
pid:<pid>@<start>:<log>handle and signal the group itself; it calls
Stopinstead. The group is signaled only while it provably is the
stage's: its leader is alive with the handle's start time, or the leader
is gone and a member of the group started no later than the stage was
last known to run, which is whenStoplast saw the leader or the
modification time of the exit status file. A group formed later under a
reused pid has only members started after that, so it is never
signaled. A group whose leader was killed before it recorded a status,
outsideStop, is left alone for the same reason, and a process that
started its own session has left the group and is not reached.Stop
now waits for the group to empty rather than for the status file, so a
stage that exits on SIGTERM no longer costs the whole grace period. The
exit status recorded beforeStopis kept.hostsandbox.Sandboxstates this for every driver:Stopends every
process the stage started that is still running, including one left
after its main process exited, andDiscardends whatStopwould.
hostsandboxtest.Runholds every driver to it, which needs a new
Subject.Orphanfield: an argv whose main process exits zero at once,
leaving a process that writes to stdout at least every 100 milliseconds
for at least the given duration. A consumer's contract test sets it; the
two new cases fail while it is nil.
v0.89.1
Fixed
egress.Gatewayrefuses a request inside a terminated tunnel whose
Host, or:authorityover HTTP/2, names another authority than the
tunnel's, answering421 Misdirected Request. The request was dialed to
the tunnel's host with that host's credentials swapped in and carried the
other name upstream, so a front that routes byHostcould deliver the
credentials to another service. Hosts compare without case, and a missing
port is 443.
v0.89.0
Added
llmdialect/bridge.Modelcarries a model's figures, so a model list
can tell a client its window and its prices:ContextWindow,
MaxOutputTokens,InputModalities, andPricing, a
bridge.ModelPricingof decimal strings per 1,000,000 tokens
(Currency,Input,Output,CachedInput,CacheWrite); a price
quoted per another count is the caller's to convert.ModelListand
ModelEntrywrite them after the members each wire's clients read:
OpenAI's ascontext_window,max_output_tokens,input_modalities
andpricing(cached_input,cache_write); Anthropic's as its own
max_input_tokensandmax_tokensbeside the sameinput_modalities
andpricing; Google's as its owninputTokenLimitand
outputTokenLimit, with no modalities or prices; the lux wire's as
the OpenAI entry, since the lux dialect's JSON is snake case. Every
pricing member writesper: 1000000. A zero figure or an empty price
is left out, so an entry without figures renders byte for byte as
before.
Changed
llmdialect/openairesp: the Responses frontend keeps a reasoning
model's reasoning across turns, so a Responses client of the gateway
no longer loses it on the way to an OpenAI Responses upstream.include: ["reasoning.encrypted_content"]setsir.Request.ReasoningReplay
instead of recordingincludeloss. An input item of typereasoning
that carriesencrypted_contentbecomes an opaque block of dialect
openai-responsesand kindreasoningin the assistant turn where it
stands, its JSON in the formir.Opaquedocuments, so the backend
replays it as the client sent it and the other backends drop it with
ir.LossOpaque(opaque) instead ofreasoning. A reasoning item
withoutencrypted_contentis still dropped with
ir.LossReasoningItems: only the upstream's store could resolve it,
and this surface stores nothing.EncodeResponsewrites such an opaque
block back as the output item it was, byte for byte, where it stands,
and a thinking block right before it no longer gets a reasoning item
of its own, since the item carries its summary. The stream does the
same: the thinking block'sresponse.output_item.addedand summary
deltas open the item, itsresponse.output_item.donecarries the
item as it came, andresponse.completedlists it; a reasoning item
without a thinking block before it gets anoutput_item.addedwith its
id and an empty summary. Theoutput_item.doneof a thinking block is
written when the next event arrives, not at itsblock_stop. The
addedframe names the item by a synthetic id, since the upstream's
id arrives only with the item; take the item from thedoneframe.
Opaque blocks of other dialects or kinds are still left out.
v0.88.0
Added
llmdialect/ir.BlockOpaque, a content block that carries one
provider item verbatim for replay to the dialect that produced it:
Block.Opaqueholds anir.Opaquewith the producingDialect, the
item'sKindwithin it, and its JSON asRaw. A backend of that
dialect writesRawunchanged where the block stands; the other
backends drop the block and recordir.LossOpaque(opaque), never an
error. The lux dialect carries it both ways as{"type":"opaque", "opaque":{"dialect","kind","raw"}}, withrawas the JSON value
itself, so a harness that stores sessions as lux JSON keeps the item
byte for byte;luxsdk.Opaqueandluxsdk.BlockOpaquere-export the
vocabulary.Rawis kept in the formencoding/jsonwrites (compact,
with<,>and&escaped), which every later marshal leaves as it
is; the lux decoder brings a body written by another encoder into that
form. In a stream the block is oneblock_startwhose header
carries the payload, then itsblock_stop. The Anthropic, Chat and
Responses frontends leave it out of the responses and streams they
write, and the Anthropic stream shifts later content indices down past
it so they stay dense.ir.PrefixCacheKeyshashes the block's
dialect, kind and raw JSON after the fields every block contributes;
keys of requests without opaque blocks are unchanged.llmdialect/ir.Request.ReasoningReplayasks for the model's reasoning
in a form the next request of the conversation can carry back, which
a reasoning model served over OpenAI Responses needs to keep its
reasoning across turns. Theopenairespbackend then writesinclude: ["reasoning.encrypted_content"](beside the logprobs member when that
is asked too) andstore: false, and keeps each reasoning item that
comes back withencrypted_contentas an opaque block of kind
reasoning, from the response body and from the stream's
response.output_item.doneframe alike, after the summary's thinking
block. Encoding a request replays such a block as its input item, and
the thinking block right before it travels inside the item instead of
being reported asthinkingloss. A reasoning item without
encrypted_contentis not kept, so a caller that does not ask sees
the same blocks and streams as before. The lux dialect carries the
ask asreasoning_replay; theanthropicbackend needs no ask, since
its thinking blocks carry their signatures; theopenaichatbackend
recordsir.LossReasoningReplay(reasoning_replay).