The Models page has no field for `compat`, and the symptom it addresses —
a gateway holding a working key at a reachable address while refusing
every request — reads as a credential or connectivity problem. Give it
the same treatment `input` already has: name the symptom, show the two
switches that account for most of it, and state the resolution order.
Refs #2646
Review found two live defects in the compat surface.
A valueless key (`supportsDeveloperRole:`) survives schemastery as null,
and resolution carried it forward as a configured value. It landed on
`Model.compat` as null, which replaced the installed catalog entry's
value and left pi-ai's `??` reaching for its baseURL detection — the
catalog layer skipped entirely, and the switch written but not applied.
The vocabulary check now refuses it where it is written, matching the
`reasoningEfforts` precedent in the same file.
The gates were keyed by protocol name, but pi-ai keys compat by type:
`openai-responses`, `azure-openai-responses`, and `openai-codex-responses`
share one `OpenAIResponsesCompat`, so two shipped catalog routes were
refused the fields their own models declare. Gates now group by compat
type, `bedrock-converse-stream` gains its own, and the protocol set is
derived from `Model.compat`'s conditional so a release that gives a
further protocol a compat type fails the gate list by name.
Field types are derived from upstream rather than restated, with a proof
pinning the profile assignable to the upstream types, so a widened value
union cannot silently narrow what configuration accepts.
The `undefined` filter stays removed: `exactOptionalPropertyTypes` keeps
a typed caller from writing one, and schemastery never materializes one,
so it was validation for a value the static interface already excludes.
Refs #2646
pi-ai infers a request's shape from the provider id and baseURL, and for
an endpoint it does not recognize it answers as though it were OpenAI
itself. A hand-declared route is by construction such an endpoint, so a
model declaring reasoningEfforts sent its system prompt as the developer
role with no configuration able to say otherwise — a gateway rejecting
that role could not be connected at all. Writing the switch anyway
validated, persisted, and was then dropped, so the misconfiguration
looked applied.
Three drift gates classify all thirty upstream compat fields as offered
or withheld, keyed by `keyof` so a pi-ai upgrade fails the build until
the new field is classified. Twenty are offered: what a private URL
cannot imply. The rest stay withheld because pi-ai's installed catalog
sets them for a named vendor.
Protocol applicability is now per field rather than per block, so
supportsDeveloperRole reaches an openai-responses route and the
anthropic-messages switches reach theirs. A compat key no protocol
declares, or one a gate withholds, is refused where it is written.
Fixes#2646
Refs #1976
Every image in history is base64-inlined into every request, so a long
session's request body grows until a gateway request-size cap rejects it
with 413 and every retry resends the same oversized body, permanently
wedging the session. Each provider route now carries maxRequestImageBytes
(default 24MiB): when the accumulated base64 image payload exceeds it,
the oldest images are replaced by a fixed model-facing placeholder until
the request fits, so the newest images survive and the session keeps
completing requests. 413 and request-body-cap wording now classify as
INVALID_REQUEST instead of the generic PI_AI_ERROR.
Fixes#2644
An image with a side above the deployed routes' 2000px many-image bound
could be durably committed by read_image, ride every later request, and
permanently fail the session with provider 400s. Admission now enforces a
configurable maxImageDimension (default 2000) during the full decode, so
read_image surfaces a recoverable tool error naming the limit instead of
poisoning durable history; the Web composer gets dedicated copy for the
new IMAGE_DIMENSION_TOO_LARGE reason.
Fixes#2626
master introduced AttachmentStore.saveImages as the batch admission
(count/aggregate-byte/media-type limits, validate-all-before-save,
ordered commit). admitEncodedImages narrows to the shared wire entry:
canonical-base64 enforcement plus delegation to saveImages, keeping one
home for batch policy while both wire endpoints (prompt RPC and the
command executor) still call one function. Test doubles gain saveImages;
batch-limit error texts follow saveImages' wording.
A claimed slash command consumed only the text half of the composer
submission: /goal with reference images executed, cleared the draft, and
silently stranded the images in the rail. Model-visible attachment intent
had no route through the command plane.
The submission envelope is now modeled end to end. CommandDefinition
input.images declares acceptance; the declaration rides the descriptor to
every client, onto the minted CommandClaim, and into the input machine's
claim snapshot. commands.execute carries the submission's base64 images
and enforces the declaration in the executor: non-declaring commands, a
missing attachment store, and exceeded batch limits settle as logged
error results before the handler runs. Admission reuses the attachment
package's new admitEncodedImages, extracted from api-proxy's prompt path
so both wire endpoints share one limits/validation/commit sequence.
Producers own model visibility: /goal submits one user followup (image
blocks + a fixed reference line) after a successful create/edit so goal
rounds read the images from session history; /plan folds them into its
steered message. Grammar misfits (/goal pause, bare /plan, /plan off)
return direct errors and the composer keeps the images.
On the client, enter adjudication carries a SubmitEnvelope and every
command route that cannot consume images throws a localized refusal that
renders as one composer notice with draft and images retained; the
claimed pre-gate applies the same copy. An accepting claim serializes the
draft images, forwards them to commands.execute, and clears plus releases
them only on a success outcome.
The assembled web test roster gains the ui-input-trigger and ui-commands
plugins, mirroring the shipped composition, so slash submissions exercise
the command plane; a new keyless snapshot pins the refusal banner and the
accepting /goal flow over the built client graph.