From 7228f4c596241c7786a53b2bd2195e411cf3d0b6 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz <25389585+poissoncorp@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:52:23 +0200 Subject: [PATCH] RDBC-1140 Add reasoning_effort to OpenAiSettings --- .../operations/ai/open_ai_settings.py | 15 +++- .../test_open_ai_reasoning_effort.py | 70 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 ravendb/tests/operations_tests/test_open_ai_reasoning_effort.py diff --git a/ravendb/documents/operations/ai/open_ai_settings.py b/ravendb/documents/operations/ai/open_ai_settings.py index f099e2ad..45e6ccf7 100644 --- a/ravendb/documents/operations/ai/open_ai_settings.py +++ b/ravendb/documents/operations/ai/open_ai_settings.py @@ -14,10 +14,17 @@ def __init__( dimensions: int = None, temperature: float = None, embeddings_max_concurrent_batches: int = None, + reasoning_effort: str = None, ): super().__init__(api_key, endpoint, model, dimensions, temperature, embeddings_max_concurrent_batches) self.organization_id = organization_id self.project_id = project_id + # The reasoning_effort sent to the provider, controlling the reasoning depth of supported models + # (such as the GPT-5 family). Typical values are "none", "minimal", "low", "medium", "high", "xhigh" + # and "max"; model families accept different subsets. Sent as supplied apart from trimming and not + # validated, so new provider values can be used directly. OpenAI accepts lowercase values only. + # When not set the field is omitted and the model applies its own default. + self.reasoning_effort = reasoning_effort @classmethod def from_json(cls, json_dict: Dict[str, Any]) -> "OpenAiSettings": @@ -32,10 +39,13 @@ def from_json(cls, json_dict: Dict[str, Any]) -> "OpenAiSettings": embeddings_max_concurrent_batches=( json_dict["EmbeddingsMaxConcurrentBatches"] if "EmbeddingsMaxConcurrentBatches" in json_dict else None ), + reasoning_effort=( + json_dict["ReasoningEffort"] if isinstance(json_dict.get("ReasoningEffort"), str) else None + ), ) def to_json(self) -> Dict[str, Any]: - return { + json_dict = { "ApiKey": self.api_key, "Endpoint": self.endpoint, "Model": self.model, @@ -45,3 +55,6 @@ def to_json(self) -> Dict[str, Any]: "ProjectId": self.project_id, "EmbeddingsMaxConcurrentBatches": self.embeddings_max_concurrent_batches, } + if self.reasoning_effort is not None and self.reasoning_effort.strip(): + json_dict["ReasoningEffort"] = self.reasoning_effort + return json_dict diff --git a/ravendb/tests/operations_tests/test_open_ai_reasoning_effort.py b/ravendb/tests/operations_tests/test_open_ai_reasoning_effort.py new file mode 100644 index 00000000..6a636343 --- /dev/null +++ b/ravendb/tests/operations_tests/test_open_ai_reasoning_effort.py @@ -0,0 +1,70 @@ +import unittest + +from ravendb.documents.operations.ai.ai_connection_string import AiConnectionString, AiModelType +from ravendb.documents.operations.ai.open_ai_settings import OpenAiSettings +from ravendb.documents.operations.connection_string.get_connection_string_operation import GetConnectionStringsOperation +from ravendb.documents.operations.connection_string.put_connection_string_operation import ( + PutConnectionStringOperation, +) +from ravendb.serverwide.server_operation_executor import ConnectionStringType +from ravendb.tests.test_base import TestBase + +_ENDPOINT = "https://api.openai.com/" + + +class TestOpenAiReasoningEffortSerialization(unittest.TestCase): + def test_reasoning_effort_is_written_when_set(self): + for effort in ["high", "xhigh", "none", "High", "future-effort"]: + with self.subTest(effort=effort): + settings = OpenAiSettings("api-key", _ENDPOINT, "gpt-5-mini", reasoning_effort=effort) + self.assertEqual(effort, settings.to_json()["ReasoningEffort"]) + + def test_reasoning_effort_is_omitted_when_not_set_or_blank(self): + for effort in [None, "", " "]: + with self.subTest(effort=effort): + settings = OpenAiSettings("api-key", _ENDPOINT, "gpt-5-mini", reasoning_effort=effort) + self.assertNotIn("ReasoningEffort", settings.to_json()) + + def test_reasoning_effort_is_read_from_json(self): + json_dict = {"ApiKey": "api-key", "Endpoint": _ENDPOINT, "Model": "gpt-5-mini", "ReasoningEffort": "High"} + self.assertEqual("High", OpenAiSettings.from_json(json_dict).reasoning_effort) + + def test_missing_or_numeric_reasoning_effort_reads_as_not_configured(self): + json_dict = {"ApiKey": "api-key", "Endpoint": _ENDPOINT, "Model": "gpt-5-mini"} + self.assertIsNone(OpenAiSettings.from_json(json_dict).reasoning_effort) + + json_dict["ReasoningEffort"] = 3 + self.assertIsNone(OpenAiSettings.from_json(json_dict).reasoning_effort) + + def test_json_round_trip(self): + settings = OpenAiSettings("api-key", _ENDPOINT, "gpt-5.2", reasoning_effort="xhigh") + self.assertEqual("xhigh", OpenAiSettings.from_json(settings.to_json()).reasoning_effort) + + +class TestOpenAiReasoningEffortConnectionString(TestBase): + def setUp(self): + super().setUp() + + def test_reasoning_effort_survives_a_connection_string_round_trip(self): + # "High" is also the shape persisted by earlier enum-based versions + for effort in ["High", "xhigh", None]: + with self.subTest(effort=effort): + connection_string = AiConnectionString( + name="reasoning-effort-round-trip", + identifier="reasoning-effort-round-trip", + model_type=AiModelType.CHAT, + openai_settings=OpenAiSettings("api-key", _ENDPOINT, "gpt-5-mini", reasoning_effort=effort), + ) + + self.store.maintenance.send(PutConnectionStringOperation(connection_string)) + + result = self.store.maintenance.send( + GetConnectionStringsOperation(connection_string.name, ConnectionStringType.AI) + ) + settings = result.ai_connection_strings[connection_string.name].openai_settings + + self.assertEqual(effort, settings.reasoning_effort) + + +if __name__ == "__main__": + unittest.main()