261 lines
12 KiB
Markdown
261 lines
12 KiB
Markdown
# Structured Generation Errors
|
|
|
|
## Purpose
|
|
|
|
Promptkit should give downstream applications actionable, machine-readable
|
|
details when the built-in OpenAI-compatible client receives a non-success HTTP
|
|
response. Today the client reports only the status code and discards the
|
|
provider response body. This makes ordinary configuration failures, such as an
|
|
unsupported strict JSON Schema keyword, unnecessarily difficult to diagnose.
|
|
|
|
This feature supplies bounded facts about the provider response. It does not
|
|
make retry, presentation, or logging decisions for consumers.
|
|
|
|
## Target End State
|
|
|
|
Every non-2xx response received by Promptkit's built-in OpenAI-compatible
|
|
client becomes a public typed generation error. A consumer can use
|
|
`errors.As` to obtain the HTTP status and any safely extracted provider fields,
|
|
and `errors.Is` continues to match `ErrLLMGenerate`.
|
|
|
|
The typed contract is available from both `Run` and `RunPrepared`. It is not
|
|
produced during preparation, which performs no model request. Successful
|
|
responses, transport failures before a response is received, cancellation,
|
|
capacity failures, validation failures, and nil responses from injected model
|
|
clients retain their existing categories and behavior.
|
|
|
|
An unusable response body never hides the known HTTP status. Empty, malformed,
|
|
unrecognized, unreadable, or oversized bodies therefore produce the same typed
|
|
error with status-only detail rather than falling back to an unstructured
|
|
error or becoming a malformed-success response.
|
|
|
|
## Public Contract
|
|
|
|
The root package exposes an immutable `GenerationError` type with unexported
|
|
state and these read-only accessors:
|
|
|
|
- `StatusCode() int` returns the received HTTP status code;
|
|
- `ProviderCode() string` returns a normalized provider code, when present;
|
|
- `ProviderType() string` returns a normalized provider error type, when
|
|
present; and
|
|
- `ProviderMessage() string` returns the bounded normalized diagnostic message,
|
|
when present.
|
|
|
|
The engine returns a `*GenerationError`, so the idiomatic inspection form is:
|
|
|
|
```go
|
|
var generationErr *promptkit.GenerationError
|
|
if errors.As(err, &generationErr) {
|
|
status := generationErr.StatusCode()
|
|
message := generationErr.ProviderMessage()
|
|
_, _ = status, message
|
|
}
|
|
```
|
|
|
|
There is no public constructor or mutation API. The type implements `error`,
|
|
unwraps to `ErrLLMGenerate`, and provides safe ordinary and Go-syntax
|
|
formatting. `Error()` and `GoString()` include the HTTP status but no provider-
|
|
controlled code, type, or message. Consumers must use the accessors
|
|
deliberately when they want provider details and must not classify failures by
|
|
matching error text.
|
|
|
|
The zero value and a nil `*GenerationError` receiver are safe: accessors return
|
|
zero or empty values, formatting returns a generic redacted generation-failure
|
|
description, and unwrapping still identifies `ErrLLMGenerate`. Engine-produced
|
|
values always have the non-2xx status received from the provider. The type has
|
|
no stable JSON representation.
|
|
|
|
All provider-derived strings remain untrusted even after normalization. GoDoc
|
|
must warn consumers that provider fields can contain sensitive request or
|
|
schema fragments and must not be logged, displayed, or returned to another
|
|
caller without an application-appropriate disclosure policy.
|
|
|
|
## Recognized Provider Envelope
|
|
|
|
Promptkit recognizes only the conventional OpenAI-compatible top-level error
|
|
object:
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"message": "diagnostic text",
|
|
"type": "invalid_request_error",
|
|
"code": "unsupported_parameter"
|
|
}
|
|
}
|
|
```
|
|
|
|
The envelope must be one JSON document followed only by JSON whitespace. The
|
|
top-level `error` value must be an object. Unknown top-level and error-object
|
|
fields are ignored. The optional supported fields are interpreted
|
|
independently:
|
|
|
|
- `message` and `type` must be JSON strings;
|
|
- `code` may be a JSON string or number and is exposed as normalized text;
|
|
numeric codes retain their validated JSON number text without floating-point
|
|
coercion; and
|
|
- `null`, booleans, arrays, objects, or otherwise invalid values are treated as
|
|
absent for that field.
|
|
|
|
An invalid optional field does not discard other valid supported fields. An
|
|
absent `error` object, malformed or multiply framed JSON, or an object with no
|
|
usable supported fields simply leaves all provider accessors empty while
|
|
preserving the typed status error.
|
|
|
|
Promptkit does not expose `param`, metadata objects, nested causes, headers, or
|
|
provider-specific extensions in this feature.
|
|
|
|
## Bounded Reading And Normalization
|
|
|
|
Non-success bodies have a separate fixed limit of 64 KiB (65,536 bytes). This
|
|
is intentionally much smaller than the successful completion-body limit while
|
|
remaining large enough for useful schema diagnostics.
|
|
|
|
- A declared `Content-Length` above the limit is rejected without reading the
|
|
body for detail extraction.
|
|
- Otherwise Promptkit reads at most one byte beyond the limit so streamed,
|
|
chunked, and underreported bodies are bounded.
|
|
- A body over the limit contributes no provider fields; Promptkit does not
|
|
parse or retain a prefix as though it were a complete envelope.
|
|
- Read failures likewise discard provider fields while preserving the status.
|
|
- The response body is closed on every outcome and is not drained beyond the
|
|
bounded read.
|
|
|
|
Extracted strings are converted to valid UTF-8, trimmed, and made single-line:
|
|
invalid UTF-8 is replaced, and runs of Unicode whitespace, control characters,
|
|
and formatting controls are replaced with one ASCII space. Empty normalized
|
|
values are treated as absent.
|
|
|
|
Normalized provider codes and types are retained only when they contain at
|
|
most 256 Unicode code points. Longer values are omitted rather than truncated
|
|
so consumers never classify on a fabricated partial identifier. A provider
|
|
message is limited to 4,096 Unicode code points; a longer normalized message is
|
|
truncated at a code-point boundary with a visible ellipsis inside that limit.
|
|
The raw response body and pre-normalized strings are never exposed or retained
|
|
in the public error.
|
|
|
|
## Error Propagation And Compatibility
|
|
|
|
- Every built-in-client non-2xx response matches `ErrLLMGenerate` and supports
|
|
`errors.As` to `*GenerationError`, including status-only cases.
|
|
- The internal model client retains its non-success-status identity for its
|
|
own package tests. The use-case layer remains provider-neutral and continues
|
|
to add only its generation category.
|
|
- The root error boundary converts only the built-in transport's structured
|
|
status error. It does not parse arbitrary error text, inspect consumer error
|
|
fields, or fabricate HTTP details for an injected `LLMClient`.
|
|
- Errors returned by injected clients remain in the chain exactly as today.
|
|
If an injected client deliberately returns an existing `*GenerationError`,
|
|
its identity may pass through ordinary wrapping, but Promptkit does not
|
|
construct or enrich one on that client's behalf.
|
|
- Existing cancellation and deadline identities, capacity errors, validation
|
|
behavior, repair behavior, and successful response decoding remain
|
|
unchanged.
|
|
- This is an additive public API. Existing consumers that use
|
|
`errors.Is(err, ErrLLMGenerate)` continue to work; consumers should not rely
|
|
on the previous rendered wording of non-success errors.
|
|
|
|
## Architecture And Ownership
|
|
|
|
The provider-envelope parser and bounded body reader belong in `internal/llm`,
|
|
which owns the OpenAI-compatible transport. The internal transport error owns
|
|
only normalized status facts and continues to match the package's existing
|
|
non-success-status sentinel.
|
|
|
|
The use-case package does not gain HTTP DTOs, status policy, or a provider-
|
|
specific branch. Its existing wrapping carries the internal error to the root
|
|
facade. The root error mapper recognizes the internal structured status error
|
|
and constructs the public `GenerationError` without exposing an internal type
|
|
or raw cause through public fields. No transport error is added to
|
|
`internal/domain`.
|
|
|
|
The public type and its exact Go semantics are owned by its declaration and
|
|
GoDoc. The
|
|
[OpenAI-compatible integration contract](../integrations/openai-compatible-chat.md)
|
|
owns recognized wire shapes, limits, and observable response behavior. The
|
|
[internal model-client document](../internal/llm.md) owns implementation flow,
|
|
internal failure categories, and test ownership. Architecture policy does not
|
|
need a new package or dependency rule for this feature.
|
|
|
|
## Documentation End State
|
|
|
|
Canonical documentation at the target state has these responsibilities:
|
|
|
|
- the `GenerationError` declaration and GoDoc define the exact public methods,
|
|
formatting, unwrapping, zero-value behavior, and trust boundary;
|
|
- `Engine.Run` and `Engine.RunPrepared` GoDoc identify the typed error without
|
|
duplicating its accessor contract;
|
|
- the consumer guide includes one short `errors.As` example and links to the
|
|
public declaration;
|
|
- the integration document replaces its status-only description with the
|
|
bounded envelope contract; and
|
|
- the internal model-client document describes parsing, conversion ownership,
|
|
and narrow test owners.
|
|
|
|
The architecture policy, framework format reference, and built-in backend
|
|
catalog do not duplicate this API or wire contract.
|
|
|
|
## Verification Expectations
|
|
|
|
Verification protects each behavior at its narrowest stable owner:
|
|
|
|
- internal model-client tests cover recognized string and numeric codes,
|
|
independent optional-field handling, unknown fields, empty and malformed
|
|
envelopes, single-document framing, read failures, declared and streamed
|
|
size boundaries, body closure, normalization, field limits, and absence of
|
|
raw provider content from rendered errors;
|
|
- root error-boundary tests cover conversion to the immutable public type,
|
|
every accessor, `errors.Is`, `errors.As`, and safe `%v`, `%+v`, and `%#v`
|
|
formatting;
|
|
- one representative ordinary run and one prepared run prove that the built-in
|
|
transport contract crosses the assembled engine boundary, without repeating
|
|
the complete parser matrix;
|
|
- existing injected-client tests continue to prove preservation of consumer
|
|
error identity without fabricated provider details; and
|
|
- all tests use controlled transports or local servers and never contact a
|
|
live or paid provider.
|
|
|
|
Security limits and their exact boundaries are contractual enough to warrant
|
|
literal boundary tests. Higher-level tests should remain representative and
|
|
must not duplicate the internal transport matrix.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- A consumer can distinguish an HTTP 400 from other generation failures and
|
|
deliberately obtain a bounded provider explanation when one is available.
|
|
- The same typed error remains available through ordinary and prepared
|
|
execution and still satisfies `errors.Is(err, ErrLLMGenerate)`.
|
|
- Default and Go-syntax error formatting cannot disclose any provider-derived
|
|
string or raw response content.
|
|
- Empty, malformed, unreadable, unrecognized, and oversized bodies preserve a
|
|
typed status-only error.
|
|
- No read, retained field, or formatted representation can exceed its stated
|
|
bound, and the body is closed on every outcome.
|
|
- Existing success, cancellation, capacity, validation, repair, and injected-
|
|
client contracts remain unchanged.
|
|
- Current-state documentation changes only when the implementation exists and
|
|
follows the repository's canonical ownership policy.
|
|
|
|
## Non-Goals
|
|
|
|
This feature does not add:
|
|
|
|
- retryability classification, retry loops, backoff, failover, or routing;
|
|
- parsing of success bodies as errors or changes to successful-response limits;
|
|
- provider-specific envelope variants beyond the conventional top-level
|
|
`error` object;
|
|
- response headers such as `Retry-After`, raw bodies, request data, endpoints,
|
|
credentials, schema documents, generated content, or provider metadata;
|
|
- logging, telemetry, redaction policy for downstream applications, HTTP status
|
|
mapping for consumer servers, or user-facing presentation;
|
|
- translation or enrichment of arbitrary injected-client errors; or
|
|
- a new public package, public constructor, mutable error value, or transport
|
|
type in the domain model.
|
|
|
|
## Open Questions
|
|
|
|
None. The public type direction, accessor surface, formatting and error-chain
|
|
behavior, envelope scope, normalization, safety limits, fallback behavior,
|
|
layer ownership, compatibility boundaries, documentation ownership, and test
|
|
boundaries are fixed by this roadmap.
|