Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
## [Unreleased]

### Fixed
- **Batch results**: Accept empty structured objects so Pydantic models can apply field defaults for OpenAI and Anthropic batch responses.
- **v2 message handling**: Preserve caller-owned message lists and nested content across request preparation and retries for OpenAI-compatible, Cohere, Mistral, OpenRouter, Writer, and xAI handlers. ([#2417](https://github.com/567-labs/instructor/issues/2417), [#2428](https://github.com/567-labs/instructor/issues/2428))
- **v2 JSON extraction**: Prefer the final complete top-level JSON value in text responses and retain every JSON object when multiple objects arrive in one streaming chunk.
- **v2 schemas**: Treat fields with Pydantic `default_factory` values as optional in generated OpenAI tool schemas.
Expand Down
2 changes: 1 addition & 1 deletion instructor/batch/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def parse_results(self, results_content: str) -> list[BatchResult]:
custom_id = data.get("custom_id", "unknown")
extracted_data = self._extract_from_response(data)

if extracted_data:
if extracted_data is not None:
try:
# Parse into response model
result = self.response_model(**extracted_data)
Expand Down
37 changes: 37 additions & 0 deletions tests/test_batch_processor_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class Person(BaseModel):
age: int


class DefaultedResult(BaseModel):
status: str = "ok"


class RecordingProvider:
def __init__(self, results: str = "") -> None:
self.results = results
Expand Down Expand Up @@ -275,6 +279,39 @@ def test_openai_results_distinguish_success_validation_extraction_and_json_error
assert results[3].raw_data == {"raw_line": "not-json"}


@pytest.mark.parametrize(
("model", "content"),
[
("openai/gpt-4.1-mini", openai_result("empty", "{}")),
(
"anthropic/claude-sonnet",
json.dumps(
{
"custom_id": "empty",
"result": {
"type": "succeeded",
"message": {"content": [{"type": "tool_use", "input": {}}]},
},
}
),
),
],
)
def test_parse_results_accepts_empty_object_for_defaulted_model(
provider: RecordingProvider,
model: str,
content: str,
) -> None:
del provider
processor = BatchProcessor(model, DefaultedResult)

results = processor.parse_results(content)

assert results == [
BatchSuccess(custom_id="empty", result=DefaultedResult(status="ok"))
]


def test_anthropic_results_support_tool_use_and_text_fallback(
provider: RecordingProvider,
) -> None:
Expand Down