Skip to content

feat: schema complexity analyzer + automatic mode selection - #2394

Open
mimran-khan wants to merge 1 commit into
567-labs:mainfrom
mimran-khan:feat/schema-analyzer-and-auto-mode
Open

feat: schema complexity analyzer + automatic mode selection#2394
mimran-khan wants to merge 1 commit into
567-labs:mainfrom
mimran-khan:feat/schema-analyzer-and-auto-mode

Conversation

@mimran-khan

@mimran-khan mimran-khan commented Jun 24, 2026

Copy link
Copy Markdown

What this does

I noticed that picking the right mode is a common friction point, especially for users with complex nested schemas. This PR adds two utilities that solve this at the library level:

analyze_schema(response_model) - inspects a Pydantic model before any API call and reports:

  • Complexity score (0-100)
  • Depth, field count, recursion detection
  • Large enum warnings
  • Actionable suggestions for schema restructuring

select_mode(response_model, provider) - picks the optimal extraction mode by cross-referencing schema complexity against provider capabilities:

  • Simple schemas -> TOOLS (leverages native function calling)
  • Deep schemas -> JSON_SCHEMA (gives the LLM explicit structure)
  • Recursive schemas -> MD_JSON/JSON (avoids tool-schema limitations)
  • Always respects what the provider actually supports

Why

I ran into this myself while testing extraction on deeply nested models. The retry loop burned tokens because the schema was too complex for TOOLS mode, but there was no guidance about that. Checking the issues list, I found similar confusion across multiple discussions.

These utilities are pure functions with no side effects, no network calls, and zero additional dependencies. They are lazy-loaded so they don't affect import performance.

Files changed

  • instructor/v2/core/schema_analyzer.py - the complexity analysis engine
  • instructor/v2/core/auto_mode.py - mode selection logic using provider specs
  • instructor/__init__.py - top-level exports (analyze_schema, SchemaAnalysis, select_mode)
  • instructor/v2/core/__init__.py - v2 core exports
  • tests/test_schema_analyzer.py - 28 tests for the analyzer
  • tests/test_auto_mode.py - 31 tests for mode selection

Usage

import instructor
from pydantic import BaseModel

class Invoice(BaseModel):
    items: list["LineItem"]
    # ... deeply nested

# Check complexity before calling
analysis = instructor.analyze_schema(Invoice)
print(f"Complexity: {analysis.complexity_score}/100")
for finding in analysis.findings:
    print(f"  [{finding.severity.value}] {finding.message}")

# Auto-select the best mode
mode = instructor.select_mode(Invoice, instructor.Provider.OPENAI)
client = instructor.from_openai(openai.OpenAI(), mode=mode)

Test results

All 59 new tests pass. Broader suite (1537 tests) has no regressions.

Closes #2393

Checklist before requesting a review

  • I have performed a self-review of my code
  • If it is a core feature, I have added thorough tests.
  • If it is a core feature, I have added documentation.

@jxnl

jxnl commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Reviewed for merge readiness. I would keep this open for #2393, but not merge it yet: it adds new public API (analyze_schema, select_mode) and hard-coded provider/mode heuristics, so this needs maintainer design approval rather than being treated as a small fix. A couple of things to tighten before review: document whether select_mode is advisory only or intended to become part of from_provider, and clean up the tests so model definitions do not rely on forward references before their classes exist / duplicate class definitions. CI should run on the refreshed branch before this gets reviewed for API shape.

Introduce two complementary utilities that help users get better extraction
results on the first attempt:

1. `analyze_schema(response_model)` inspects a Pydantic model's JSON schema
   and reports complexity metrics (depth, field count, recursion, large enums)
   along with actionable findings and a 0-100 complexity score.

2. `select_mode(response_model, provider)` picks the optimal extraction mode
   for a given provider by cross-referencing schema complexity with provider
   capabilities. Handles recursive schemas, deep nesting, and high-complexity
   patterns by routing to the mode most likely to succeed.

Both are exposed at `instructor.analyze_schema` and `instructor.select_mode`
for convenient top-level access.
@mimran-khan
mimran-khan force-pushed the feat/schema-analyzer-and-auto-mode branch from 8972db1 to bc55f4f Compare June 29, 2026 14:28
@mimran-khan

Copy link
Copy Markdown
Author

Thanks for the feedback. I've addressed both points:

  1. Advisory-only documentation: Updated the module docstring and select_mode docstring to explicitly state this is advisory-only. It does not get called internally by from_provider or any client factory. Users opt in by passing the returned mode manually. The intent is a starting point for users unsure which mode fits their schema, not an automatic override.

  2. Test cleanup: Reordered all model definitions in both test files so dependencies are defined before usage (no forward references). Removed the duplicate ModelWithLargeEnum class that existed in test_schema_analyzer.py.

Branch is force-pushed so CI should pick it up fresh.

jxnl added a commit that referenced this pull request Aug 3, 2026
## Summary

- accumulate declared, nested, and unknown numeric OpenAI/Anthropic
usage fields across retries while preserving non-numeric metadata
- add corrective feedback when a Responses API retry receives no tool
call
- preserve raw iterable type hints through sync and async v2
parallel-tool wrappers
- strengthen API-key-free coverage for current and future SDK usage
counters

## Consolidated and superseded items

- closes #2493
- consolidates contributor work from #2498, #2500, and #2501 with
original commit authorship preserved
- supersedes #2497 because it drops unknown `model_extra` counters
- supersedes #2499 because its hand-maintained provider field lists
would drift as SDKs evolve

## Validation

- focused changed-surface suite: `110 passed`
- broad offline v2/coverage suite: `2368 passed, 91 skipped, 73
deselected`
- Ruff check and format check: passed
- scoped `ty check`: passed
- `uv lock --check`: passed
- pre-commit hooks and `git diff --check`: passed

The 73 deselected tests require live provider credentials. An unfiltered
local run confirmed its 22 failures were provider network connections in
the restricted environment; GitHub provider jobs remain the
authoritative validation for those paths.

## Intentionally skipped

- provider additions or expansions: #2436, #2435, #2423, #2409, #2384,
#2322, #2306, #2298, #2283, #2168, #2086; issues #2408, #2383, #2365,
#2260, #2084, #2076
- broad architecture, product, security, or streaming decisions: #2394,
#2392, #2357, #2356, #2355, #2351, #2321, #2307, #2287, #2263; issues
#2479, #2403, #2393, #2391, #2316, #2272, #2056
- dependency batch: #2433
- nontrivial examples and editorial/resource additions: #2468, #2405,
#2401, #2354, #2346, #2311, #2305; issue #2404

These remain open because they need dedicated product, architecture,
provider, security, dependency, or editorial review and are not required
for the `1.15.5` patch release.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes retry usage totals and reask message content on failure paths;
scope is limited and heavily covered by tests, with no auth or
data-store changes.
> 
> **Overview**
> Bundles three v2 retry and wrapper fixes for a patch release.
> 
> **Retry usage accounting** replaces hand-maintained token field sums
with generic `_accumulate_models` on Pydantic usage objects. Numeric
fields (including nested models and `model_extra` counters) add across
retries; booleans and other non-numeric metadata are not treated as
billable. OpenAI and Anthropic paths share this logic.
> 
> **OpenAI Responses reask** appends a user correction when
`RESPONSES_TOOLS` validation fails but the output has no tool calls
(e.g. reasoning-only), so retries include feedback instead of repeating
the same request.
> 
> **Parallel tools** in `patch_v2` skips `prepare_response_model` and
does not replace `response_model` with the handler’s prepared wrapper
for parallel modes, keeping raw `Iterable[...]` hints so schemas and
parsed results include every member type.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
bbddca1. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

jxnl commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

1.16 triage update: keeping this open, not merging it in the maintenance release. The analyzer may be separable, but select_mode still codifies product policy without provider/model outcome data, and this branch's Bedrock assumptions are already stale relative to fully green draft #2515 (which adds explicit Bedrock JSON_SCHEMA/TOOLS_STRICT support). Please treat a schema-analyzer-only API and mode recommendation policy as separate review decisions after the release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: schema complexity pre-flight analysis and automatic mode selection

2 participants