Commit 961ff8c
authored
Fix tuple schema compatibility with OpenAI structured output API (#619)
* Make LLM tests fully parametric in model name via EFFECTFUL_LLM_MODEL env var
Replace provider-specific environment variable checks (OPENAI_API_KEY,
ANTHROPIC_API_KEY) and skip markers (requires_openai, requires_anthropic)
with a single EFFECTFUL_LLM_MODEL environment variable that controls
which model is used for all LLM integration tests.
- Remove hardcoded model names from all test files in favor of LLM_MODEL
read from EFFECTFUL_LLM_MODEL env var
- Replace requires_openai/requires_anthropic markers with requires_llm
- Remove model parametrization that was cross-provider; tests now use
whichever model the env var specifies
- Use litellm.supports_vision() to conditionally skip vision tests
- Remove default model from LiteLLMProvider (make model required)
- Update CI workflow to pass EFFECTFUL_LLM_MODEL as a matrix parameter,
making it easy to add parallel CI stages for different providers
- Rename/remove fixture files to match updated test names
Closes #589
* Deduplicate LLM_MODEL/requires_llm into conftest, fix import order, rename test
- Move LLM_MODEL and requires_llm definitions to tests/conftest.py;
all four LLM test files now import from there
- Fix import ordering in test_handlers_llm_encoding.py (stdlib before local)
- Rename test_agent_tool_names_are_openai_compatible_integration to
test_agent_tool_names_are_valid_integration since it's no longer
OpenAI-specific
* Address review feedback: fix breaking change, import hygiene, redundancy
- Restore LiteLLMProvider default model via env var fallback so existing
callers (template.py, test_handlers_llm_template.py, notebook) are not
broken: model=os.environ.get("EFFECTFUL_LLM_MODEL", "gpt-4o")
- Move LLM_MODEL/requires_llm from conftest.py to tests/_llm_helpers.py
since conftest.py should not be imported directly
- Fix import placement in test_handlers_llm_provider.py (was between
module-level constants instead of grouped with imports)
- Remove redundant @requires_llm on vision tests where the skipif
condition already covers the not-LLM_MODEL case
* Revert LiteLLMProvider default to plain literal "gpt-4o"
The env var belongs in test infrastructure, not the library API.
LiteLLMProvider should have a clean, explicit default.
* Minor fix
* minor fixes
* minor
* Make LLM test suite parametric in model name (#589)
Remove provider-specific environment variable checks and skip markers
from LLM tests in favor of a single EFFECTFUL_LLM_MODEL env var.
- Add LLM_MODEL and requires_llm to tests/conftest.py; LLM_MODEL
defaults to "gpt-4o-mini" and is overridable via EFFECTFUL_LLM_MODEL
- Live tests (tool calling, encoding, agent tool names) use LLM_MODEL
and are gated by requires_llm which checks for any provider API key
- Integration tests use make_provider() which returns a live
LiteLLMProvider when API keys are available, else falls back to
ReplayLiteLLMProvider for offline replay from fixtures
- Replay-only tests (simple_prompt_multiple_models, cross_endpoint,
caching) keep hardcoded model names and always run since they never
call the API
- Update CI workflow to pass EFFECTFUL_LLM_MODEL as a matrix parameter
for easy parallel stages with different providers
Closes #589
* Minor fix
* Minor fix
* Fix tuple type schemas for OpenAI structured output compatibility
Three fixes in encoding.py:
1. TupleEncodable.encode() now returns a TupleItems model instance
(not a raw tuple), and deserialize() returns the model directly.
This fixes pydantic validation in litellm integration tests for
NamedTuple and fixed-tuple types.
2. Add _TupleSafeJsonSchema that overrides pydantic's tuple_schema()
to produce object schemas (item_0, item_1 properties) instead of
prefixItems arrays. Applied via _BoxEncoding.model_json_schema()
so dataclasses containing tuple fields produce OpenAI-compatible
schemas.
3. SequenceEncodable.encode() returns a list (not tuple) to preserve
encode idempotency — nested_type on a list dispatches to the
sequence encoder, avoiding a mismatch with TupleEncodable.
Also adds test_handlers_llm_encoding.py back to CI workflow.
* Clean up
* Split out tuple encoding fixes to separate branch
Revert encoding.py to master and remove encoding tests from CI
workflow. Tuple schema fixes will be in a dedicated PR.
* Fix tuple type schemas for OpenAI structured output compatibility
Three fixes in encoding.py:
1. TupleEncodable.encode() returns a TupleItems model instance (not a
raw tuple), and deserialize() returns the model directly. This fixes
pydantic validation in litellm integration tests for NamedTuple and
fixed-tuple types. Extract shared field access into _extract_items().
2. Add _TupleSafeJsonSchema that overrides pydantic's tuple_schema() to
produce object schemas (item_0, item_1 properties) instead of
prefixItems arrays. Applied via _BoxEncoding.model_json_schema() so
dataclasses containing tuple fields produce OpenAI-compatible schemas.
Can be removed once #584 replaces the Encodable system.
3. SequenceEncodable.encode()/deserialize() return lists (not tuples) to
preserve encode idempotency — nested_type on a list dispatches to the
sequence encoder, avoiding a mismatch with TupleEncodable.
* Fix tuple type schemas for OpenAI structured output compatibility
Use Pydantic's Annotated extension points (PlainValidator, PlainSerializer,
WithJsonSchema) to handle tuples — adapted from _pydantic_type_tuple in #584.
- _safe_tuple_type: rewrites fixed-length tuple[T1, T2, ...] into an
Annotated type with custom validation (item_N dict → tuple), serialization
(tuple → item_N dict), and JSON schema (object with item_0/item_1
properties instead of prefixItems). Single mechanism for schema, validation,
and serialization.
- _rewrite_tuple_annotations: recursively applies _safe_tuple_type to
dataclass fields, creating an Annotated proxy so nested tuples inside
objects also get safe schemas.
- TupleEncodable.encode() returns TupleItems model instances; deserialize()
returns the model directly. Shared field access via _extract_items().
- SequenceEncodable.encode()/deserialize() return lists to preserve encode
idempotency with the new TupleEncodable return type.
* Replace Encodable ABC with TypeToPydanticType
Replace the hand-rolled Encodable ABC and its 10+ subclasses with a
recursive type rewriter (TypeToPydanticType) that produces Annotated
wrappers with Pydantic validators/serializers. This fixes #626 (tuple
fields in dataclasses) and #631 (Callable fields in dataclasses) by
recursively rewriting field annotations before Pydantic sees them.
- Add TypeEvaluator ABC to unification.py for recursive type traversal
- Rewrite encoding.py: Encodable[T] now returns Annotated types via
TypeToPydanticType, used with pydantic.TypeAdapter
- Update completions.py: Encodable.define() -> TypeAdapter(Encodable[T])
- Update template.py: extract _collect_tools to completions.py
- Update all test files for new API
* Fix composite type encoding and update provider tests
- Add field rewriting for dataclass and BaseModel types whose fields
contain special types (Callable, tuple, Image, etc.). Uses a proxy
Pydantic model for validation/serialization while preserving the
original type identity on roundtrip. Fixes #631.
- Add additionalProperties:false to all object schemas in _inline_refs
for OpenAI strict mode compatibility.
- Update test_handlers_llm_provider.py for new encoding API:
response_format=Encodable.define(T) -> response_type=T, tools= -> env=
- Add composite type regression tests for #626 and #631.
- Fix ruff formatting in test_internals_unification.py.
* Add encoding integration tests to LLM CI workflow
Include test_handlers_llm_encoding.py in the LLM integration test
workflow so the litellm_completion tests (which need API keys) run in CI.
* Remove unused type: ignore comment in encoding.py
* Removing unnecessary files
* Minor fix
* Fix CI failures: mypy, test mocks, and rebuild fixtures
- Remove unused type: ignore in unification.py
- Fix _call_assistant mock signature (tools/response_format -> env/response_type)
- Fix mock tool call args to remove old {"value": ...} wrapping
- Fix test_synthesized_function_roundtrip to use dump_python(mode="json")
- Rebuild all provider test fixtures with gpt-4o for new API format
- Fix caching test fixtures for deterministic replay
* Fix template test expectations for unwrapped encoding format
- Update template formatting tests: int values no longer wrapped in {"value": N}
- Fix tool call mock args in template test (remove {"value": ...} wrapping)
* Add _strict_json_schema for OpenAI strict mode compliance
- Add _strict_json_schema() to completions.py (provider boundary)
- Apply to tool specs in call_assistant
- Wrap response_format schema in object wrapper for encoding integration tests
- Apply _strict_json_schema to tool spec tests
* Fix lint: restore type: ignore, sort imports, remove unused import
* Fix strict schema: inline $refs, handle arrays, dual mypy ignore
- Enhance _strict_json_schema to inline $ref/$defs recursively
- Add items fallback for arrays (required by OpenAI strict mode)
- Handle prefixItems → items conversion for fixed-length tuples
- Use dual type: ignore[attr-defined,unused-ignore] for CI/local mypy compat
- Fix xfail pattern matching to use startswith for precise case filtering
* Fix ruff formatting in test files
* Xfail image/tool/dtc types in LLM provider integration tests
Image types can't roundtrip through LLM (returns URLs, not data URIs).
Tool/DecodedToolCall types have schemas incompatible with OpenAI strict
mode when used as nested parameter types. Apply PROVIDER_CASES (with
xfails) to tool-as-param and tool-as-return tests, and extend the
pattern to catch composite image types (e.g. tuple-img-str, list-img).
* Use ROUNDTRIP_CASES for tool-as-return test (no xfails needed)
Return types don't affect the tool spec schema sent to OpenAI, so
all cases pass without xfails. Only tool-as-param needs PROVIDER_CASES.
* Separate xfail sets for response_model vs tool-as-param tests
- response_model: xfail img/tool/dtc (LLM can't return images, Tool
schema incompatible with strict mode)
- tool-as-param: xfail only tool/dtc (image types produce valid param
schemas)
- tool-as-return: no xfails needed (return type not in tool spec)
* Fix mypy segfault under xdist: group mypy tests on same worker
The mypy type-check tests call mypy in-process. When xdist schedules
them on separate workers simultaneously, mypy's C extensions segfault.
Use xdist_group("mypy") to ensure all mypy tests run on the same
worker, and set --dist loadgroup as default to respect the grouping.
* Removing a bunch of duplicated features, change tests so that Dataclass encodable is manually defined
* Remove strict json schema in favor of openai internal tools
* Inline named tuple to pydantic_type_tuple
* Replace _force_additional_properties_false with _remove_additional_properties_true
Symmetric to litellm.utils._remove_additional_properties (which removes false
for Vertex AI). Strips additionalProperties: true from litellm/OpenAI models
that use extra="allow", then lets _ensure_strict_json_schema apply false.
Also switch DecodedToolCall schema to OpenAI's ChatCompletionMessageToolCall
(has actual fields: id, function, type) instead of litellm's (empty dict).
Remove tool/dtc xfails — schemas are now strict-mode compatible.
* Use OpenAI's ChatCompletionMessageToolCall for tool call validation
Litellm's ChatCompletionMessageToolCall has no fields (extra="allow"),
so model_validate is a no-op. Switch to OpenAI's type which validates
id, function.name, function.arguments fields. Also fix serializer to
use type="function" (OpenAI const) and json.dumps for arguments.
* Replace _ensure_strict_json_schema with _make_strict_safe for Tool/DecodedToolCall
_ensure_strict_json_schema forces all properties into required, breaking
schemas with optional fields (e.g. ChatCompletionToolParam's parameters,
description, strict, cache_control). _make_strict_safe makes optional
properties nullable before requiring them, so they're safe for OpenAI
strict mode.
Also removes _ensure_strict_json_schema from Image and Callable handlers
where it was unnecessary, and provides encoded tool context in the
response_model integration test so the LLM can produce valid
DecodedToolCall instances.
* Suppress mypy error for runtime Encodable[type(v)] in test
* Xfail Tool/DecodedToolCall integration tests, revert _make_strict_safe
Tool/DecodedToolCall as response_model or tool parameter are
fundamentally incompatible with OpenAI strict mode (optional fields,
bare "type": "object" without properties). This matches master's
behavior which also xfailed these cases.
Removes _make_strict_safe, restores _ensure_strict_json_schema from
openai for Tool/DecodedToolCall WithJsonSchema schemas.
* Remove _remove_additional_properties_true helper
Only used for Tool/DecodedToolCall WithJsonSchema schemas which are
already xfailed in integration tests. No test depends on it.
* formatting1 parent c67f1f5 commit 961ff8c
46 files changed
Lines changed: 1606 additions & 1469 deletions
File tree
- .github/workflows
- effectful
- handlers/llm
- internals
- tests
- fixtures
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
27 | | - | |
28 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
29 | 39 | | |
30 | 40 | | |
31 | 41 | | |
| |||
59 | 69 | | |
60 | 70 | | |
61 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
62 | 76 | | |
63 | 77 | | |
64 | | - | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
65 | 81 | | |
66 | 82 | | |
67 | | - | |
| 83 | + | |
68 | 84 | | |
69 | 85 | | |
70 | | - | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
71 | 89 | | |
72 | 90 | | |
73 | 91 | | |
| |||
160 | 178 | | |
161 | 179 | | |
162 | 180 | | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
163 | 212 | | |
164 | 213 | | |
165 | 214 | | |
| |||
172 | 221 | | |
173 | 222 | | |
174 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
175 | 228 | | |
176 | | - | |
177 | | - | |
178 | | - | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
179 | 232 | | |
180 | 233 | | |
181 | 234 | | |
| |||
190 | 243 | | |
191 | 244 | | |
192 | 245 | | |
| 246 | + | |
193 | 247 | | |
194 | | - | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
195 | 252 | | |
196 | 253 | | |
197 | | - | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
198 | 264 | | |
199 | 265 | | |
200 | | - | |
201 | | - | |
| 266 | + | |
| 267 | + | |
202 | 268 | | |
203 | 269 | | |
204 | 270 | | |
| |||
212 | 278 | | |
213 | 279 | | |
214 | 280 | | |
215 | | - | |
216 | | - | |
217 | | - | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
218 | 285 | | |
219 | | - | |
| 286 | + | |
220 | 287 | | |
221 | 288 | | |
222 | 289 | | |
| |||
231 | 298 | | |
232 | 299 | | |
233 | 300 | | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
240 | 310 | | |
241 | 311 | | |
242 | 312 | | |
| |||
256 | 326 | | |
257 | 327 | | |
258 | 328 | | |
259 | | - | |
260 | | - | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
261 | 334 | | |
262 | | - | |
263 | 335 | | |
264 | 336 | | |
265 | 337 | | |
| |||
295 | 367 | | |
296 | 368 | | |
297 | 369 | | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
| 370 | + | |
| 371 | + | |
303 | 372 | | |
304 | | - | |
| 373 | + | |
| 374 | + | |
305 | 375 | | |
306 | 376 | | |
307 | 377 | | |
| |||
327 | 397 | | |
328 | 398 | | |
329 | 399 | | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
335 | | - | |
336 | | - | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
| 400 | + | |
| 401 | + | |
345 | 402 | | |
346 | 403 | | |
347 | 404 | | |
| |||
404 | 461 | | |
405 | 462 | | |
406 | 463 | | |
407 | | - | |
| 464 | + | |
408 | 465 | | |
409 | | - | |
410 | | - | |
| 466 | + | |
| 467 | + | |
411 | 468 | | |
412 | 469 | | |
413 | 470 | | |
414 | 471 | | |
415 | 472 | | |
416 | | - | |
417 | | - | |
418 | | - | |
419 | 473 | | |
420 | | - | |
| 474 | + | |
421 | 475 | | |
422 | 476 | | |
423 | 477 | | |
| |||
461 | 515 | | |
462 | 516 | | |
463 | 517 | | |
464 | | - | |
465 | | - | |
| 518 | + | |
| 519 | + | |
466 | 520 | | |
467 | 521 | | |
468 | 522 | | |
469 | 523 | | |
470 | 524 | | |
471 | 525 | | |
472 | 526 | | |
473 | | - | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
474 | 532 | | |
475 | 533 | | |
476 | 534 | | |
| |||
479 | 537 | | |
480 | 538 | | |
481 | 539 | | |
482 | | - | |
| 540 | + | |
483 | 541 | | |
484 | 542 | | |
485 | 543 | | |
486 | 544 | | |
487 | 545 | | |
488 | 546 | | |
489 | | - | |
| 547 | + | |
490 | 548 | | |
491 | 549 | | |
492 | 550 | | |
0 commit comments