diff --git a/litellm/llms/ovhcloud/chat/transformation.py b/litellm/llms/ovhcloud/chat/transformation.py index 4100c548f2c..77d3683566c 100644 --- a/litellm/llms/ovhcloud/chat/transformation.py +++ b/litellm/llms/ovhcloud/chat/transformation.py @@ -5,17 +5,17 @@ Our unified API follows the OpenAI standard. More information on our website: https://endpoints.ai.cloud.ovh.net """ -from typing import Optional, Union, List +from typing import Any, Optional, Union, List import httpx -from litellm.utils import ModelResponseStream +from litellm.utils import ModelResponse, ModelResponseStream from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig from litellm.llms.ovhcloud.utils import OVHCloudException from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.llms.base_llm.chat.transformation import LiteLLMLoggingObj from litellm.types.llms.openai import AllMessageValues - class OVHCloudChatConfig(OpenAIGPTConfig): @property def custom_llm_provider(self) -> Optional[str]: @@ -75,6 +75,50 @@ class OVHCloudChatConfig(OpenAIGPTConfig): return response + def transform_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ModelResponse, + logging_obj: LiteLLMLoggingObj, + request_data: dict, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ModelResponse: + # Call parent to do standard OpenAI response parsing + model_response = super().transform_response( + model=model, + raw_response=raw_response, + model_response=model_response, + logging_obj=logging_obj, + request_data=request_data, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + encoding=encoding, + api_key=api_key, + json_mode=json_mode, + ) + + # OVHCloud field migration (deadline: 2026-05-11): + # `reasoning_content` is replaced by `reasoning` in non-streaming responses. + # Normalise to `reasoning_content` so downstream consumers + # see a consistent key during the transition window. + for choice in model_response.choices: + message = getattr(choice, "message", None) + if message is not None: + reasoning_new = getattr(message, "reasoning", None) + reasoning_legacy = getattr(message, "reasoning_content", None) + if reasoning_new is not None and reasoning_legacy is None: + message.reasoning_content = reasoning_new + + return model_response + + class OVHCloudChatCompletionStreamingHandler(BaseModelResponseIterator): """ Handler for OVHCloud AI Endpoints streaming chat completion responses diff --git a/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py b/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py index 88ce3b4c296..b112f6d87f1 100644 --- a/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py +++ b/tests/test_litellm/llms/ovhcloud/test_ovhcloud_chat_transformation.py @@ -4,7 +4,7 @@ Unit tests for OVHCloud AI Endpoints chat integration. import os import sys - +import litellm import pytest from litellm.llms.ovhcloud.utils import OVHCloudException @@ -364,4 +364,52 @@ class TestOVHCloudReasoningFieldMigration: ], } result = handler.chunk_parser(chunk) - assert result.choices[0]["delta"]["reasoning_content"] == "legacy field" \ No newline at end of file + assert result.choices[0]["delta"]["reasoning_content"] == "legacy field" + + + def test_non_streaming_new_reasoning_field(self): + """Non-streaming: new `reasoning` field should be mapped to `reasoning_content`.""" + from unittest.mock import MagicMock, patch + import json + + config = OVHCloudChatConfig() + + raw_response = MagicMock() + raw_response.status_code = 200 + raw_response.headers = {"Content-Type": "application/json"} + raw_response.text = json.dumps({ + "id": "test-id", + "object": "chat.completion", + "created": 1234567890, + "model": "test-model", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello!", + "reasoning": "Let me think...", + }, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + }) + raw_response.json.return_value = json.loads(raw_response.text) + + model_response = litellm.ModelResponse() + + result = config.transform_response( + model="ovhcloud/test-model", + raw_response=raw_response, + model_response=model_response, + logging_obj=MagicMock(), + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding=None, + api_key="test-key", + ) + + assert result.choices[0].message.reasoning_content == "Let me think..." \ No newline at end of file