Commit 5f6f1e1
authored
feat(llm): image content blocks (proposal 0015) (#44)
* feat(llm): add ProviderUnsupportedContentBlock error category
Adds the provider_unsupported_content_block canonical category from
llm-provider §7 (introduced by proposal 0015). Raised when the bound
model does not support a content block type used in the request
(e.g., a text-only model received an image block, or the model
supports images but not the requested media_type or source variant).
The exception carries block_type and reason attributes so callers
can route on the specific unsupported case; mirrors the precedent
StructuredOutputInvalid set in PR-1 (carry the structured payload
the caller needs for diagnostics + recovery).
Non-transient by default — NOT added to TRANSIENT_CATEGORIES. The
bound model's capability set doesn't change between calls, so
retrying without changing the request, the bound model, or the
provider won't succeed. Users who want fallback semantics MAY route
on the category in a userland middleware (e.g., switch to a
multimodal-capable provider).
Distinct from ProviderInvalidRequest: ProviderInvalidRequest covers
spec-shape violations (the request is malformed); this category
covers capability mismatches (the request is well-formed but the
bound model can't fulfill it).
* feat(llm): content-block types + UserMessage extension
Adds the content-block surface from llm-provider §3.1 (proposal 0015):
- TextBlock(type, text) with a non-empty-text validator
- ImageSourceURL(type, url) and ImageSourceInline(type, base64_data),
joined by an ImageSource discriminated union over the source's
``type`` field
- ImageBlock(type, source, media_type, detail) with a validator that
rejects inline sources missing a media_type. detail defaults to
None so the wire omits the field unless explicitly set (providers
apply their own conceptual default of "auto"); the docstring spells
out the subtle case of an explicit detail="auto"
- ContentBlock discriminated union over TextBlock | ImageBlock
UserMessage.content becomes ``str | list[ContentBlock]``. The existing
_check_content validator extends to enforce the non-empty rule on
both shapes. Other roles (system, assistant, tool) stay text-string
only — content blocks are user-only in v1 per the spec.
media_type is typed as ``str | None`` (not a Literal of the three
guaranteed types) so callers can pass additional image/* types
providers document support for.
* feat(llm/openai): content-array wire mapping + content-rejection mapping
Two extensions in OpenAIProvider for proposal 0015:
- _message_to_wire's user case now branches on content shape: string
maps directly (the v0.4.0 form); a content-block sequence maps to
OpenAI's content-array form per §8.1.1 via the new _block_to_wire
helper. TextBlock → {type: "text", text}. ImageBlock(URL) →
{type: "image_url", image_url: {url, detail?}}. ImageBlock(inline)
constructs an RFC 2397 data: URI from media_type + base64_data and
routes through the same image_url entry shape. The detail hint goes
on the wire only when the spec block has it set (None on the spec
block omits it from the wire; providers apply their own default of
"auto" per §3.1.2).
- classify_http_error's 400 branch now routes content-rejection
bodies to ProviderUnsupportedContentBlock rather than the generic
ProviderInvalidRequest. Detection is a heuristic on error.code
(known set: image_content_not_supported,
unsupported_image_media_type, audio_content_not_supported,
video_content_not_supported, unsupported_content_block; plus an
image+not_supported substring fallback), error.type
(image_parse_error, image_content_not_supported), and
error.message ("does not support" + image/audio/video). The spec
is implementation-defined on the detection rule (§8.3); the
heuristic lives inline so it's evolvable as OpenAI's error-code
surface shifts.
_extract_rejected_block_type pulls a best-effort "image" / "audio"
/ "video" identifier out of the error code or message for surfacing
on ProviderUnsupportedContentBlock.block_type.
* test(conformance): drive 0015 fixtures 009-020
Removes the 12 deferred-skip rows for content-block fixtures from
both _DEFERRED_FIXTURES dicts (test_llm_provider.py runtime + the
test_fixture_parsing.py typed parser).
_build_message in test_llm_provider.py extends the user case to
pass raw["content"] through (str or list) unchanged; Pydantic's
discriminated union on the content-block ``type`` field parses each
dict in the list to the right TextBlock / ImageBlock variant
automatically.
LlmCallSpec.messages in harness/directives.py is already typed as
list[dict[str, Any]] (permissive), so the typed parser accepts the
content-block list-of-dicts shape without model extensions. The
parsing tests slip past for the 009-020 fixtures via the same path
PR-1's 021-028 used.
All 28 llm-provider conformance fixtures now pass (the prior 16
plus the 12 new content-block ones). Full suite: 515 pass, 72
skipped (down from 84 — only the 16 deferred fixtures for
proposals 0011 / 0014 / 0017 remain).
* test+docs: content-block unit tests + docs + CHANGELOG entry
Adds tests/unit/test_content_blocks.py (24 tests) covering bits
the conformance fixtures don't exercise directly:
- TextBlock / ImageBlock construction validation (non-empty text,
inline-needs-media_type, detail enum, URL source can skip
media_type)
- UserMessage construction from dict-form content blocks (the path
the conformance test fixture loader uses)
- _block_to_wire mapping for text, URL with/without detail, inline
base64 (RFC 2397 data URI construction)
- classify_http_error 400 routing to ProviderUnsupportedContentBlock
via the heuristic; negative cases (unrelated 400 stays
ProviderInvalidRequest)
- _extract_rejected_block_type picks up "image" / "audio" from
error.code or error.message
Docs:
- docs/concepts/llms.md: new "Content blocks (multimodal user
messages)" section between Structured output and Routing,
covering the two content shapes, URL vs inline sources, the
detail hint, and the new ProviderUnsupportedContentBlock category.
- docs/model-providers/index.md: errors table extended to 9
categories with the new row + a Behaviour-guarantees note that
OpenAIProvider does post-receive detection only; pre-send is a
userland-middleware pattern.
- docs/model-providers/authoring.md: "Beyond the skeleton" gains
a content-blocks entry pointing custom-provider authors at the
multimodal wire mapping + the unsupported-content category.
CHANGELOG [Unreleased] gains 3 entries: the user-message content
extension, the OpenAI wire mapping, and the new error category. All
in the same release as PR-1's 0016 entries per the consolidated-
release strategy.
* fix: CoPilot review pass on PR #44
- audio/video symmetry in the substring fallback of
_looks_like_content_rejection
- explicit isinstance(block, ImageBlock) guard in _block_to_wire to
surface added union variants as a TypeError instead of an
AttributeError on .source
- clarify ImageBlock.media_type docstring: permitted but redundant on
URL sources (the URL payload carries content-type), provider
implementations MAY consume it as a hint
- reword CHANGELOG qualifier '(proposal X, spec vY.Z)' →
'(proposal X, introduced in spec vY.Z)' on the 0015 and 0016 entries
so it doesn't read like a per-entry submodule pin change1 parent 05806b2 commit 5f6f1e1
11 files changed
Lines changed: 730 additions & 37 deletions
File tree
- docs
- concepts
- model-providers
- src/openarmature/llm
- providers
- tests
- conformance
- unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
224 | 335 | | |
225 | 336 | | |
226 | 337 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
198 | 198 | | |
199 | 199 | | |
200 | 200 | | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
201 | 211 | | |
202 | 212 | | |
203 | 213 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
79 | 80 | | |
80 | 81 | | |
81 | 82 | | |
82 | 83 | | |
83 | | - | |
84 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
85 | 96 | | |
86 | 97 | | |
87 | 98 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| 44 | + | |
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
47 | 54 | | |
48 | 55 | | |
| 56 | + | |
49 | 57 | | |
50 | 58 | | |
51 | 59 | | |
| |||
69 | 77 | | |
70 | 78 | | |
71 | 79 | | |
| 80 | + | |
72 | 81 | | |
73 | 82 | | |
74 | 83 | | |
| 84 | + | |
75 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
76 | 90 | | |
77 | 91 | | |
78 | 92 | | |
| |||
85 | 99 | | |
86 | 100 | | |
87 | 101 | | |
| 102 | + | |
88 | 103 | | |
89 | 104 | | |
90 | 105 | | |
91 | 106 | | |
| 107 | + | |
92 | 108 | | |
93 | 109 | | |
94 | 110 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| |||
137 | 138 | | |
138 | 139 | | |
139 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
140 | 183 | | |
141 | 184 | | |
142 | 185 | | |
| |||
184 | 227 | | |
185 | 228 | | |
186 | 229 | | |
| 230 | + | |
187 | 231 | | |
188 | 232 | | |
189 | 233 | | |
| |||
194 | 238 | | |
195 | 239 | | |
196 | 240 | | |
| 241 | + | |
197 | 242 | | |
198 | 243 | | |
0 commit comments