Skip to content

Commit 26e1795

Browse files
committed
docs(v2): add mode migration guide and normalization tests
- Add docs/concepts/mode-migration.md explaining legacy mode deprecation - Add tests/v2/test_mode_normalization.py for mode normalization logic - Update mkdocs.yml with mode migration guide link - Tests skip gracefully when handlers not yet registered This PR was written by [Cursor](https://cursor.com)
1 parent 64b7a3e commit 26e1795

File tree

3 files changed

+389
-0
lines changed

3 files changed

+389
-0
lines changed

docs/concepts/mode-migration.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
title: Mode Migration Guide
3+
description: Migrate from provider-specific modes to the core modes in Instructor.
4+
---
5+
6+
# Mode Migration Guide
7+
8+
This guide helps you move from provider-specific modes to the core modes.
9+
Core modes work across providers and are the recommended choice for new code.
10+
11+
## Core Modes
12+
13+
These are the core modes you should use:
14+
15+
- `TOOLS`: Tool or function calling
16+
- `JSON_SCHEMA`: Native schema support when a provider has it
17+
- `MD_JSON`: JSON extracted from text or code blocks
18+
- `PARALLEL_TOOLS`: Multiple tool calls in one response
19+
- `RESPONSES_TOOLS`: OpenAI Responses API tools
20+
21+
## Quick Mapping
22+
23+
Use this table to replace legacy modes:
24+
25+
| Legacy Mode | Core Mode |
26+
|------------|-----------|
27+
| `FUNCTIONS` | `TOOLS` |
28+
| `TOOLS_STRICT` | `TOOLS` |
29+
| `ANTHROPIC_TOOLS` | `TOOLS` |
30+
| `ANTHROPIC_JSON` | `MD_JSON` |
31+
| `COHERE_TOOLS` | `TOOLS` |
32+
| `COHERE_JSON_SCHEMA` | `JSON_SCHEMA` |
33+
| `XAI_TOOLS` | `TOOLS` |
34+
| `XAI_JSON` | `MD_JSON` |
35+
| `MISTRAL_TOOLS` | `TOOLS` |
36+
| `MISTRAL_STRUCTURED_OUTPUTS` | `JSON_SCHEMA` |
37+
| `FIREWORKS_TOOLS` | `TOOLS` |
38+
| `FIREWORKS_JSON` | `MD_JSON` |
39+
| `CEREBRAS_TOOLS` | `TOOLS` |
40+
| `CEREBRAS_JSON` | `MD_JSON` |
41+
| `WRITER_TOOLS` | `TOOLS` |
42+
| `WRITER_JSON` | `MD_JSON` |
43+
| `BEDROCK_TOOLS` | `TOOLS` |
44+
| `BEDROCK_JSON` | `MD_JSON` |
45+
| `PERPLEXITY_JSON` | `MD_JSON` |
46+
| `VERTEXAI_TOOLS` | `TOOLS` |
47+
| `VERTEXAI_JSON` | `MD_JSON` |
48+
| `VERTEXAI_PARALLEL_TOOLS` | `PARALLEL_TOOLS` |
49+
50+
## Example: Anthropic
51+
52+
**Before:**
53+
54+
```python
55+
import instructor
56+
from instructor import Mode
57+
58+
client = instructor.from_provider(
59+
"anthropic/claude-3-5-haiku-latest",
60+
mode=Mode.ANTHROPIC_TOOLS,
61+
)
62+
```
63+
64+
**After:**
65+
66+
```python
67+
import instructor
68+
from instructor import Mode
69+
70+
client = instructor.from_provider(
71+
"anthropic/claude-3-5-haiku-latest",
72+
mode=Mode.TOOLS,
73+
)
74+
```
75+
76+
## Example: Bedrock
77+
78+
**Before:**
79+
80+
```python
81+
import instructor
82+
from instructor import Mode
83+
84+
client = instructor.from_provider(
85+
"bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0",
86+
mode=Mode.BEDROCK_TOOLS,
87+
)
88+
```
89+
90+
**After:**
91+
92+
```python
93+
import instructor
94+
from instructor import Mode
95+
96+
client = instructor.from_provider(
97+
"bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0",
98+
mode=Mode.TOOLS,
99+
)
100+
```
101+
102+
## Notes
103+
104+
- Legacy modes still work but show a deprecation warning.
105+
- Use core modes for new code and docs.
106+
- Core tests are parameterized by provider and mode for consistent coverage.
107+
- See [Mode Comparison](../modes-comparison.md) for details.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ nav:
249249
- Patching: 'concepts/patching.md'
250250
- from_provider: 'concepts/from_provider.md'
251251
- Migration Guide: 'concepts/migration.md'
252+
- Mode Migration: 'concepts/mode-migration.md'
252253
- Hooks: 'concepts/hooks.md'
253254
- Types: 'concepts/types.md'
254255
- TypedDicts: 'concepts/typeddicts.md'
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
5+
import pytest
6+
7+
from instructor.mode import Mode
8+
from instructor.utils.providers import Provider
9+
10+
try:
11+
from instructor.v2.core import (
12+
DEPRECATED_MODE_MAPPING,
13+
mode_registry,
14+
normalize_mode,
15+
reset_deprecation_warnings,
16+
)
17+
except ModuleNotFoundError:
18+
pytest.skip("v2 module not available", allow_module_level=True)
19+
20+
21+
@pytest.fixture(autouse=True)
22+
def reset_warnings():
23+
"""Reset deprecation warnings before each test."""
24+
reset_deprecation_warnings()
25+
yield
26+
reset_deprecation_warnings()
27+
28+
29+
@pytest.mark.parametrize(
30+
"provider,mode,expected",
31+
[
32+
# GenAI legacy modes
33+
(Provider.GENAI, Mode.GENAI_TOOLS, Mode.TOOLS),
34+
(Provider.GENAI, Mode.GENAI_JSON, Mode.JSON),
35+
(Provider.GENAI, Mode.GENAI_STRUCTURED_OUTPUTS, Mode.JSON),
36+
# Anthropic legacy modes
37+
(Provider.ANTHROPIC, Mode.ANTHROPIC_TOOLS, Mode.TOOLS),
38+
(Provider.ANTHROPIC, Mode.ANTHROPIC_JSON, Mode.MD_JSON),
39+
(Provider.ANTHROPIC, Mode.ANTHROPIC_PARALLEL_TOOLS, Mode.PARALLEL_TOOLS),
40+
# OpenAI legacy modes
41+
(Provider.OPENAI, Mode.FUNCTIONS, Mode.TOOLS),
42+
(Provider.OPENAI, Mode.TOOLS_STRICT, Mode.TOOLS),
43+
(Provider.OPENAI, Mode.JSON_O1, Mode.JSON_SCHEMA),
44+
# Note: Mode.JSON is NOT deprecated - it's used by GenAI as a valid mode
45+
(
46+
Provider.OPENAI,
47+
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
48+
Mode.RESPONSES_TOOLS,
49+
),
50+
# Mistral legacy modes
51+
(Provider.MISTRAL, Mode.MISTRAL_TOOLS, Mode.TOOLS),
52+
(Provider.MISTRAL, Mode.MISTRAL_STRUCTURED_OUTPUTS, Mode.JSON_SCHEMA),
53+
# Cohere legacy modes
54+
(Provider.COHERE, Mode.COHERE_TOOLS, Mode.TOOLS),
55+
(Provider.COHERE, Mode.COHERE_JSON_SCHEMA, Mode.JSON_SCHEMA),
56+
# xAI legacy modes
57+
(Provider.XAI, Mode.XAI_TOOLS, Mode.TOOLS),
58+
(Provider.XAI, Mode.XAI_JSON, Mode.MD_JSON),
59+
# Generic modes should pass through unchanged
60+
(Provider.GENAI, Mode.TOOLS, Mode.TOOLS),
61+
(Provider.GENAI, Mode.JSON, Mode.JSON),
62+
(Provider.ANTHROPIC, Mode.TOOLS, Mode.TOOLS),
63+
(Provider.ANTHROPIC, Mode.JSON, Mode.JSON),
64+
(Provider.OPENAI, Mode.TOOLS, Mode.TOOLS),
65+
(Provider.OPENAI, Mode.JSON_SCHEMA, Mode.JSON_SCHEMA),
66+
(Provider.OPENAI, Mode.MD_JSON, Mode.MD_JSON),
67+
(Provider.OPENAI, Mode.PARALLEL_TOOLS, Mode.PARALLEL_TOOLS),
68+
(Provider.OPENAI, Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS),
69+
],
70+
)
71+
def test_normalize_mode(provider: Provider, mode: Mode, expected: Mode):
72+
"""Test that mode normalization works correctly."""
73+
result = normalize_mode(provider, mode)
74+
assert result == expected
75+
76+
77+
@pytest.mark.parametrize(
78+
"provider,expected_modes",
79+
[
80+
(Provider.GENAI, [Mode.TOOLS, Mode.JSON]),
81+
(
82+
Provider.OPENAI,
83+
[
84+
Mode.TOOLS,
85+
Mode.JSON_SCHEMA,
86+
Mode.MD_JSON,
87+
Mode.PARALLEL_TOOLS,
88+
Mode.RESPONSES_TOOLS,
89+
],
90+
),
91+
],
92+
)
93+
def test_handlers_registered_with_generic_modes(
94+
provider: Provider, expected_modes: list[Mode]
95+
):
96+
"""Test that handlers are registered with generic modes.
97+
98+
Note: This test requires provider handlers to be registered.
99+
It will be fully enabled in PR 3+ when providers are added.
100+
"""
101+
# Skip if no modes are registered for this provider yet
102+
registered_modes = mode_registry.get_modes_for_provider(provider)
103+
if not registered_modes:
104+
pytest.skip(f"No handlers registered for {provider.value} yet")
105+
106+
for mode in expected_modes:
107+
if mode in registered_modes:
108+
assert mode_registry.is_registered(provider, mode)
109+
110+
111+
@pytest.mark.parametrize(
112+
"provider,legacy_mode,expected_mode",
113+
[
114+
(Provider.GENAI, Mode.GENAI_TOOLS, Mode.TOOLS),
115+
(Provider.GENAI, Mode.GENAI_JSON, Mode.JSON),
116+
(Provider.GENAI, Mode.GENAI_STRUCTURED_OUTPUTS, Mode.JSON),
117+
(Provider.OPENAI, Mode.FUNCTIONS, Mode.TOOLS),
118+
(Provider.OPENAI, Mode.TOOLS_STRICT, Mode.TOOLS),
119+
(Provider.OPENAI, Mode.JSON_O1, Mode.JSON_SCHEMA),
120+
(
121+
Provider.OPENAI,
122+
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
123+
Mode.RESPONSES_TOOLS,
124+
),
125+
],
126+
)
127+
def test_backwards_compatibility(
128+
provider: Provider, legacy_mode: Mode, expected_mode: Mode
129+
):
130+
"""Test that old provider-specific modes still work.
131+
132+
Note: This test requires provider handlers to be registered.
133+
It will be fully enabled in PR 3+ when providers are added.
134+
"""
135+
# Skip if provider doesn't have any handlers registered yet
136+
registered_modes = mode_registry.get_modes_for_provider(provider)
137+
if not registered_modes:
138+
pytest.skip(f"No handlers registered for {provider.value} yet")
139+
140+
# The normalized mode should be registered
141+
assert mode_registry.is_registered(provider, expected_mode)
142+
143+
# Legacy mode should normalize and find the same handler
144+
legacy_handler = mode_registry.get_handler_class(provider, legacy_mode)
145+
expected_handler = mode_registry.get_handler_class(provider, expected_mode)
146+
147+
assert legacy_handler is not None
148+
assert expected_handler is not None
149+
assert legacy_handler == expected_handler
150+
151+
152+
@pytest.mark.parametrize(
153+
"provider,mode,expected_replacement",
154+
[
155+
(Provider.OPENAI, Mode.FUNCTIONS, Mode.TOOLS),
156+
(Provider.OPENAI, Mode.TOOLS_STRICT, Mode.TOOLS),
157+
(Provider.OPENAI, Mode.JSON_O1, Mode.JSON_SCHEMA),
158+
# Note: Mode.JSON is NOT deprecated - it's used by GenAI as a valid mode
159+
(Provider.ANTHROPIC, Mode.ANTHROPIC_TOOLS, Mode.TOOLS),
160+
(Provider.GENAI, Mode.GENAI_TOOLS, Mode.TOOLS),
161+
(Provider.MISTRAL, Mode.MISTRAL_TOOLS, Mode.TOOLS),
162+
(Provider.COHERE, Mode.COHERE_TOOLS, Mode.TOOLS),
163+
(Provider.XAI, Mode.XAI_TOOLS, Mode.TOOLS),
164+
],
165+
)
166+
def test_deprecated_mode_emits_warning(
167+
provider: Provider, mode: Mode, expected_replacement: Mode
168+
):
169+
"""Test that using deprecated modes emits a deprecation warning."""
170+
with warnings.catch_warnings(record=True) as w:
171+
warnings.simplefilter("always")
172+
result = normalize_mode(provider, mode)
173+
174+
# Should have emitted a warning
175+
assert len(w) == 1
176+
assert issubclass(w[0].category, DeprecationWarning)
177+
assert mode.name in str(w[0].message)
178+
assert expected_replacement.name in str(w[0].message)
179+
assert "v3.0" in str(w[0].message)
180+
181+
# Should still return the correct normalized mode
182+
assert result == expected_replacement
183+
184+
185+
@pytest.mark.parametrize("provider", [Provider.OPENAI, Provider.ANTHROPIC])
186+
def test_deprecated_mode_warning_only_once(provider: Provider):
187+
"""Test that deprecation warning is only shown once per mode."""
188+
with warnings.catch_warnings(record=True) as w:
189+
warnings.simplefilter("always")
190+
191+
# First call should warn
192+
normalize_mode(provider, Mode.FUNCTIONS)
193+
assert len(w) == 1
194+
195+
# Second call should not warn again
196+
normalize_mode(provider, Mode.FUNCTIONS)
197+
assert len(w) == 1 # Still only 1 warning
198+
199+
# Different mode should warn
200+
normalize_mode(provider, Mode.TOOLS_STRICT)
201+
assert len(w) == 2
202+
203+
204+
@pytest.mark.parametrize(
205+
"provider,modes",
206+
[
207+
(Provider.OPENAI, [Mode.TOOLS, Mode.JSON_SCHEMA, Mode.MD_JSON]),
208+
(Provider.GENAI, [Mode.TOOLS, Mode.JSON]),
209+
(Provider.ANTHROPIC, [Mode.TOOLS, Mode.JSON]),
210+
],
211+
)
212+
def test_generic_mode_no_warning(provider: Provider, modes: list[Mode]):
213+
"""Test that using generic modes does not emit warnings."""
214+
with warnings.catch_warnings(record=True) as w:
215+
warnings.simplefilter("always")
216+
217+
for mode in modes:
218+
normalize_mode(provider, mode)
219+
220+
# No warnings should be emitted for generic modes
221+
assert len(w) == 0
222+
223+
224+
def test_deprecated_mode_mapping_is_complete():
225+
"""Test that all provider-specific modes are in the deprecated mapping."""
226+
# These are the provider-specific modes that should be deprecated
227+
# Note: Mode.JSON is NOT deprecated - it's used by GenAI as a valid mode
228+
expected_deprecated = {
229+
# OpenAI legacy
230+
Mode.FUNCTIONS,
231+
Mode.TOOLS_STRICT,
232+
# Mode.JSON is NOT deprecated - it's used by GenAI
233+
Mode.JSON_O1,
234+
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
235+
# Anthropic
236+
Mode.ANTHROPIC_TOOLS,
237+
Mode.ANTHROPIC_JSON,
238+
Mode.ANTHROPIC_PARALLEL_TOOLS,
239+
# GenAI
240+
Mode.GENAI_TOOLS,
241+
Mode.GENAI_JSON,
242+
Mode.GENAI_STRUCTURED_OUTPUTS,
243+
# Mistral
244+
Mode.MISTRAL_TOOLS,
245+
Mode.MISTRAL_STRUCTURED_OUTPUTS,
246+
# Cohere
247+
Mode.COHERE_TOOLS,
248+
Mode.COHERE_JSON_SCHEMA,
249+
# xAI
250+
Mode.XAI_TOOLS,
251+
Mode.XAI_JSON,
252+
# Fireworks
253+
Mode.FIREWORKS_TOOLS,
254+
Mode.FIREWORKS_JSON,
255+
# Cerebras
256+
Mode.CEREBRAS_TOOLS,
257+
Mode.CEREBRAS_JSON,
258+
# Writer
259+
Mode.WRITER_TOOLS,
260+
Mode.WRITER_JSON,
261+
# Bedrock
262+
Mode.BEDROCK_TOOLS,
263+
Mode.BEDROCK_JSON,
264+
# Perplexity
265+
Mode.PERPLEXITY_JSON,
266+
# VertexAI
267+
Mode.VERTEXAI_TOOLS,
268+
Mode.VERTEXAI_JSON,
269+
Mode.VERTEXAI_PARALLEL_TOOLS,
270+
# Gemini
271+
Mode.GEMINI_TOOLS,
272+
Mode.GEMINI_JSON,
273+
# OpenRouter
274+
Mode.OPENROUTER_STRUCTURED_OUTPUTS,
275+
}
276+
277+
# Check that all expected deprecated modes are in the mapping
278+
for mode in expected_deprecated:
279+
assert mode in DEPRECATED_MODE_MAPPING, (
280+
f"Mode.{mode.name} should be in DEPRECATED_MODE_MAPPING"
281+
)

0 commit comments

Comments
 (0)