From 2cdb026d4eafa9eb5d70324c5ecc2c3b97a62648 Mon Sep 17 00:00:00 2001 From: rezaho Date: Wed, 9 Sep 2026 18:54:11 +0200 Subject: [PATCH] fix: preserve configured reasoning effort in OpenAI OAuth requests --- src/marsys/models/adapters/openai_oauth.py | 12 +++ .../test_openai_oauth_reasoning_effort.py | 89 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/models/test_openai_oauth_reasoning_effort.py diff --git a/src/marsys/models/adapters/openai_oauth.py b/src/marsys/models/adapters/openai_oauth.py index d770f5ed..24af07e4 100644 --- a/src/marsys/models/adapters/openai_oauth.py +++ b/src/marsys/models/adapters/openai_oauth.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple from marsys.models.adapters.base import APIProviderAdapter, AsyncBaseAPIAdapter +from marsys.models.adapters.openai import thinking_budget_to_effort from marsys.models.response_models import ( ErrorResponse, HarmonizedResponse, @@ -362,6 +363,17 @@ def format_request_payload( "include": ["reasoning.encrypted_content"], } + # Summary controls visibility; depth uses the same effort/budget contract + # as the API-key Responses adapter. + reasoning_effort = kwargs.get("reasoning_effort") + if not reasoning_effort: + reasoning_effort = thinking_budget_to_effort(kwargs.get("thinking_budget")) + if reasoning_effort and reasoning_effort.lower() in ["minimal", "low", "medium", "high"]: + effort = reasoning_effort.lower() + if effort == "minimal" and "codex" in self.model_name.lower(): + effort = "low" + payload["reasoning"]["effort"] = effort + if kwargs.get("prompt_cache_key") is not None: payload["prompt_cache_key"] = kwargs["prompt_cache_key"] diff --git a/tests/models/test_openai_oauth_reasoning_effort.py b/tests/models/test_openai_oauth_reasoning_effort.py new file mode 100644 index 00000000..997d0b87 --- /dev/null +++ b/tests/models/test_openai_oauth_reasoning_effort.py @@ -0,0 +1,89 @@ +"""Configured depth survives BaseAPIModel and the actual OAuth payload builder.""" + +import pytest + +from marsys.models.adapters.openai_oauth import OpenAIOAuthAdapter +from marsys.models.models import BaseAPIModel + + +MESSAGES = [{"role": "user", "content": "Continue the work."}] + + +@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize( + "configured, overrides, expected", + [ + ({"reasoning_effort": "low"}, {}, "low"), + ({"reasoning_effort": "medium"}, {}, "medium"), + ({"reasoning_effort": "high"}, {}, "high"), + ({"reasoning_effort": "minimal"}, {}, "minimal"), + ({"reasoning_effort": "LOW"}, {}, "low"), + ({"reasoning_effort": "high"}, {"reasoning_effort": "low"}, "low"), + ({"reasoning_effort": "high"}, {"reasoning_effort": None}, None), + ({}, {}, None), + ({"thinking_budget": 0}, {}, None), + ({"thinking_budget": -1}, {}, None), + ({"thinking_budget": 512}, {}, "minimal"), + ({"thinking_budget": 1024}, {}, "low"), + ({"thinking_budget": 8192}, {}, "medium"), + ({"thinking_budget": 32768}, {}, "high"), + ({"thinking_budget": 32768, "reasoning_effort": "low"}, {}, "low"), + ({"thinking_budget": 0, "reasoning_effort": "low"}, {}, "low"), + ], +) +async def test_model_depth_reaches_oauth_transport( + monkeypatch, asynchronous, configured, overrides, expected +): + monkeypatch.setattr( + OpenAIOAuthAdapter, + "_load_codex_credentials", + lambda self, path: {"access_token": "fake-token", "account_id": "fake-account"}, + ) + model = BaseAPIModel( + model_name="gpt-5", provider="openai-oauth", api_key="unused", base_url="", + credentials_path="unused-fake-credentials", auto_refresh=False, **configured, + ) + captured = [] + + def capture(endpoint, headers, payload): + captured.append((endpoint, headers, payload)) + return { + "output": [{"type": "message", "content": [{"type": "output_text", "text": "ok"}]}], + "usage": {}, + } + + async def async_capture(endpoint, headers, payload, **kwargs): + return capture(endpoint, headers, payload) + + monkeypatch.setattr(model.adapter, "_sync_stream_response", capture) + monkeypatch.setattr(model.async_adapter, "_async_stream_response", async_capture) + for _ in range(2): + if asynchronous: + await model.arun(MESSAGES, prompt_cache_key="install:owner", **overrides) + else: + model.run(MESSAGES, prompt_cache_key="install:owner", **overrides) + + assert len(captured) == 2 + assert captured[0] == captured[1] + endpoint, headers, payload = captured[0] + reasoning = {"summary": "auto"} + if expected is not None: + reasoning["effort"] = expected + assert payload["reasoning"] == reasoning + assert payload["prompt_cache_key"] == "install:owner" + assert payload["stream"] is True + assert payload["store"] is False + assert payload["include"] == ["reasoning.encrypted_content"] + assert "reasoning_effort" not in payload + assert "thinking_budget" not in payload + assert endpoint == OpenAIOAuthAdapter.RESPONSES_ENDPOINT + assert headers["Authorization"] == "Bearer fake-token" + assert headers["chatgpt-account-id"] == "fake-account" + + +@pytest.mark.parametrize("kwargs", [{"reasoning_effort": "minimal"}, {"thinking_budget": 512}]) +def test_codex_smallest_effort_matches_existing_openai_mapping(kwargs): + adapter = object.__new__(OpenAIOAuthAdapter) + adapter.model_name = "gpt-5.3-codex" + payload = adapter.format_request_payload(MESSAGES, **kwargs) + assert payload["reasoning"] == {"summary": "auto", "effort": "low"}