Repository navigation
Warn when a provider does not honor the schema - #348
Merged
Merged
Conversation
A strict structured-output attempt that finishes on stop, with no stop sequence, and returns content that is not a JSON object is something constrained decoding cannot produce. Observability section 5.5.7 requires one WARNING record for each such attempt, carrying the event name openarmature.structured_output.schema_not_honored, the provider, the bound model, the call id and the attempt index. The OpenAI provider runs the check per wire attempt, ahead of the attempt event's dispatch, so it logs with or without an invocation or observers, and a reask that recovers still leaves the failed attempt's record. It reads each attempt's own request for the path and the stop sequences, the mapped finish_reason, and the content through the same strict parser the validation uses. The record is advisory: nothing is retried or switched to the fallback because of it.
There was a problem hiding this comment.
🟡 Changes recommended
The stop-sequence check uses raw configuration instead of the reconciled request value, causing incorrect warning behavior for malformed extras and empty strings; reconciliation logic and regression tests are needed.
1 open issue
What changed in this PR
Adds an advisory warning when a strict structured-output attempt returns content that is not a JSON object, implementing part 4 of proposal 0126 without changing retry or fallback behavior.
Changes:
- Emits a named warning per qualifying attempt, carrying provider, model, call ID and attempt index.
- Shares strict JSON parsing between validation and warning detection.
- Adds provider and OpenTelemetry tests, exports the diagnostic name, and updates documentation and conformance notes.
| File | Description |
|---|---|
tests/unit/test_observability_otel.py |
Verifies warning export, attributes and correlation through the log bridge. |
tests/unit/test_llm_provider.py |
Tests warning conditions, exclusions and per-attempt behavior. |
src/openarmature/observability/diagnostics.py |
Defines the diagnostic event-name constant. |
src/openarmature/observability/__init__.py |
Exports the new constant publicly. |
src/openarmature/llm/providers/openai.py |
Adds warning detection and emission using shared JSON parsing. |
docs/concepts/observability.md |
Lists the warning in the diagnostic event table. |
docs/concepts/llms.md |
Explains warning conditions and advisory behavior. |
conformance.toml |
Records implementation progress and deferred fixture coverage. |
CHANGELOG.md |
Documents the warning and its metadata. |
Review effort: Balanced
The schema_not_honored check decided whether an attempt carried a stop sequence from the config: the declared stop_sequences and the extras' stop. The request builder reconciles the two, so the config can disagree with the wire. With stop_sequences=[] and a malformed extras stop such as [123], the builder drops the extra and sends stop: [], yet the check saw a stop sequence and stayed silent. An extras stop of "" went out as a string while the check treated it as absent. The check now reads stop from the body each attempt built, and counts any string or non-empty list as a stop sequence, since observability section 5.5.7 exempts only an empty list.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Part 4 of proposal 0126 (spec v0.119.0). The OpenAI provider now logs one
WARNINGwhen a schema-constrained attempt returns content that constrained decoding cannot produce (observability §5.5.7)."native"path (strict: true), failedstructured_output_invalid, finished withfinish_reason"stop", carried no stop sequence (declared, orstopin extras, where an empty list counts as none), and returned content that is not a JSON object: malformed JSON, or JSON whose top level is not an object. A JSON object that fails validation does not fire it. Neither doeslength,content_filter(an OpenAI refusal included), a stop sequence, or any other path.openarmature.structured_output.schema_not_honored(§7),openarmature.llm.provider,openarmature.llm.model(the bound model, not the one the response reports),openarmature.llm.call_id, which joins it to the call's typed event, andopenarmature.llm.attempt_index. Through the log bridge it also getsopenarmature.correlation_id, and inside a node body the node span's trace and span ids.StructuredOutputInvalid. Nothing is retried or switched to the fallback because of it.What changes
STRUCTURED_OUTPUT_SCHEMA_NOT_HONOREDfromopenarmature.observability.Verification
mkdocs build --strictare clean.native_unenforcedas native, ignore either stop channel or count an empty list as a stop sequence, parse with plainjson.loads, count an array as an object, move the check after the dispatch check, fire once per call, read the base request instead of the attempt's, hardcode the provider, fix the attempt index, drop the call id or the event name, and log atINFO.install_log_bridge, and checks the exported log record's event name field, attributes and correlation id.