Skip to content

Commit 30b10c6

Browse files
derekhigginsDerek Higgins
andauthored
fix(inference, anthropic): default json_schema.strict to true (#6024)
Fixes #6020 ## Summary - Default `json_schema.strict` to `false` when it is `None`, so it survives `model_dump(exclude_none=True)` serialization - Prevents Anthropic's API from rejecting the request with a 400 due to missing `strict` field ## Test plan - [x] Unit test for strict field defaulting - [ ] Integration test with Anthropic provider and structured output <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/ogx-ai/ogx/pull/6024" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review"> </picture> </a> <!-- devin-review-badge-end --> --------- Signed-off-by: Derek Higgins <derekh@redhat.com> Co-authored-by: Derek Higgins <dhiggins@example.com>
1 parent 828bb93 commit 30b10c6

2 files changed

Lines changed: 92 additions & 3 deletions

File tree

src/ogx/providers/remote/inference/anthropic/anthropic.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
from .config import AnthropicConfig
2121

2222

23+
def _make_schema_strict(schema: dict) -> None:
24+
"""Recursively add additionalProperties: false to all object schemas for strict mode compliance."""
25+
if schema.get("type") == "object":
26+
if "additionalProperties" not in schema:
27+
schema["additionalProperties"] = False
28+
for prop in (schema.get("properties") or {}).values():
29+
_make_schema_strict(prop)
30+
31+
2332
class AnthropicInferenceAdapter(OpenAIMixin):
2433
"""Inference adapter for Anthropic Claude models."""
2534

@@ -56,6 +65,18 @@ async def openai_chat_completion(
5665
p = func.get("parameters")
5766
if isinstance(p, dict) and not p:
5867
func["parameters"] = {"type": "object"}
68+
if (
69+
params.response_format
70+
and hasattr(params.response_format, "json_schema")
71+
and params.response_format.json_schema
72+
):
73+
js = params.response_format.json_schema
74+
# Anthropic requires strict: true for json_schema response format
75+
if js.get("strict") is None:
76+
js["strict"] = True
77+
schema = js.get("schema")
78+
if js["strict"] and isinstance(schema, dict):
79+
_make_schema_strict(schema)
5980
return await super().openai_chat_completion(params)
6081

6182
async def openai_completion(

tests/unit/providers/inference/test_anthropic_adapter.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
# This source code is licensed under the terms described in the LICENSE file in
55
# the root directory of this source tree.
66

7-
from unittest.mock import AsyncMock, patch
7+
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
88

99
import pytest
1010

11-
from ogx.providers.remote.inference.anthropic.anthropic import AnthropicInferenceAdapter
11+
from ogx.providers.remote.inference.anthropic.anthropic import AnthropicInferenceAdapter, _make_schema_strict
1212
from ogx.providers.remote.inference.anthropic.config import AnthropicConfig
13-
from ogx_api.inference.models import OpenAIChatCompletionRequestWithExtraBody
13+
from ogx_api import Model, OpenAIChatCompletionRequestWithExtraBody, OpenAIUserMessageParam
14+
from ogx_api.inference.models import OpenAIJSONSchema, OpenAIResponseFormatJSONSchema
1415

1516

1617
@pytest.fixture
@@ -40,3 +41,70 @@ async def test_empty_tool_parameters_normalized(adapter, input_params, expected_
4041
await adapter.openai_chat_completion(params)
4142

4243
assert params.tools[0]["function"]["parameters"] == expected_params
44+
45+
46+
async def _empty_stream():
47+
if False:
48+
yield None
49+
50+
51+
async def test_chat_completion_defaults_strict_true_when_none():
52+
adapter = AnthropicInferenceAdapter(config=AnthropicConfig(api_key="test-key"))
53+
adapter.__provider_id__ = "anthropic"
54+
adapter.model_store = AsyncMock()
55+
adapter.model_store.get_model.return_value = Model(
56+
identifier="test-model",
57+
provider_id="anthropic",
58+
provider_resource_id="test-model",
59+
)
60+
61+
mock_client = MagicMock()
62+
captured_params = {}
63+
64+
async def _capture_create(**kwargs):
65+
captured_params.update(kwargs)
66+
return _empty_stream()
67+
68+
mock_client.chat.completions.create = _capture_create
69+
70+
with patch.object(type(adapter), "client", new_callable=PropertyMock, return_value=mock_client):
71+
params = OpenAIChatCompletionRequestWithExtraBody(
72+
model="test-model",
73+
messages=[OpenAIUserMessageParam(role="user", content="test")],
74+
response_format=OpenAIResponseFormatJSONSchema(
75+
json_schema=OpenAIJSONSchema(
76+
name="test",
77+
schema={"type": "object", "properties": {"a": {"type": "string"}}},
78+
),
79+
),
80+
)
81+
82+
await adapter.openai_chat_completion(params)
83+
84+
assert captured_params["response_format"]["json_schema"]["strict"] is True
85+
assert captured_params["response_format"]["json_schema"]["schema"]["additionalProperties"] is False
86+
87+
88+
def test_make_schema_strict_adds_additional_properties():
89+
schema = {
90+
"type": "object",
91+
"properties": {
92+
"name": {"type": "string"},
93+
"age": {"type": "integer"},
94+
},
95+
}
96+
_make_schema_strict(schema)
97+
assert schema["additionalProperties"] is False
98+
assert "required" not in schema
99+
100+
101+
def test_make_schema_strict_preserves_existing():
102+
schema = {
103+
"type": "object",
104+
"properties": {"a": {"type": "string"}},
105+
"additionalProperties": True,
106+
"required": ["a"],
107+
}
108+
_make_schema_strict(schema)
109+
assert schema["additionalProperties"] is True
110+
assert schema["required"] == ["a"]

0 commit comments

Comments
 (0)