From a27604b67a98576d105facc21553a0f2e69b729f Mon Sep 17 00:00:00 2001 From: gotsysdba Date: Sat, 30 Aug 2025 20:57:57 +0100 Subject: [PATCH 1/3] Fixes #14090 --- litellm/types/llms/oci.py | 6 +- .../oci/chat/test_oci_chat_transformation.py | 76 +++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/litellm/types/llms/oci.py b/litellm/types/llms/oci.py index 52c1b2943f..e3ea07463e 100644 --- a/litellm/types/llms/oci.py +++ b/litellm/types/llms/oci.py @@ -121,7 +121,7 @@ class OCICompletionTokenDetails(BaseModel): reasoningTokens: int -class OCIPropmtTokensDetails(BaseModel): +class OCIPromptTokensDetails(BaseModel): """Prompt token details in the OCI response.""" cachedTokens: int @@ -133,8 +133,8 @@ class OCIResponseUsage(BaseModel): promptTokens: int completionTokens: int totalTokens: int - completionTokensDetails: OCICompletionTokenDetails - promptTokensDetails: OCIPropmtTokensDetails + completionTokensDetails: Optional[OCICompletionTokenDetails] = None + promptTokensDetails: Optional[OCIPromptTokensDetails] = None class OCIResponseChoice(BaseModel): diff --git a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py index 547d4bf807..cbf2ca356e 100644 --- a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py +++ b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py @@ -305,3 +305,79 @@ class TestOCIChatConfig: assert usage.prompt_tokens == 10 # type: ignore assert usage.completion_tokens == 20 # type: ignore assert usage.total_tokens == 30 # type: ignore + +def test_transform_response_with_missing_usage_details(self): + """ + Tests that usage details default to None if they are missing or null in the API response. + """ + config = OCIChatConfig() + created_time = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z") + + # Case 1: usage fields completely missing + mock_oci_response_missing = { + "modelId": TEST_MODEL_NAME, + "modelVersion": "1.0", + "chatResponse": { + "apiFormat": "GENERIC", + "choices": [], + "timeCreated": created_time, + "usage": { + "promptTokens": 5, + "completionTokens": 8, + "totalTokens": 13, + # missing completionTokensDetails and promptTokensDetails + }, + }, + } + + response_missing = httpx.Response(status_code=200, json=mock_oci_response_missing) + result_missing = config.transform_response( + model=TEST_MODEL_NAME, + raw_response=response_missing, + model_response=ModelResponse(), + logging_obj={}, + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding={}, + ) + + usage_missing = result_missing.usage # type: ignore + assert usage_missing.completionTokensDetails is None + assert usage_missing.promptTokensDetails is None + + # Case 2: usage fields explicitly null + mock_oci_response_null = { + "modelId": TEST_MODEL_NAME, + "modelVersion": "1.0", + "chatResponse": { + "apiFormat": "GENERIC", + "choices": [], + "timeCreated": created_time, + "usage": { + "promptTokens": 5, + "completionTokens": 8, + "totalTokens": 13, + "completionTokensDetails": None, + "promptTokensDetails": None, + }, + }, + } + + response_null = httpx.Response(status_code=200, json=mock_oci_response_null) + result_null = config.transform_response( + model=TEST_MODEL_NAME, + raw_response=response_null, + model_response=ModelResponse(), + logging_obj={}, + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding={}, + ) + + usage_null = result_null.usage # type: ignore + assert usage_null.completionTokensDetails is None + assert usage_null.promptTokensDetails is None \ No newline at end of file From 1ae5466daf021883c8e6cd8ed17820363b7fac51 Mon Sep 17 00:00:00 2001 From: gotsysdba Date: Sat, 30 Aug 2025 21:15:54 +0100 Subject: [PATCH 2/3] Test Case --- .../oci/chat/test_oci_chat_transformation.py | 108 +++++++----------- 1 file changed, 42 insertions(+), 66 deletions(-) diff --git a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py index cbf2ca356e..3151bed109 100644 --- a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py +++ b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py @@ -306,78 +306,54 @@ class TestOCIChatConfig: assert usage.completion_tokens == 20 # type: ignore assert usage.total_tokens == 30 # type: ignore -def test_transform_response_with_missing_usage_details(self): +def test_oci_response_usage_handles_missing_and_null_fields(self): """ - Tests that usage details default to None if they are missing or null in the API response. + Test that OCIResponseUsage parses correctly when + completionTokensDetails and promptTokensDetails are missing or null. """ - config = OCIChatConfig() - created_time = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z") + from litellm.llms.oci.chat.transformation import OCIResponseUsage - # Case 1: usage fields completely missing - mock_oci_response_missing = { - "modelId": TEST_MODEL_NAME, - "modelVersion": "1.0", - "chatResponse": { - "apiFormat": "GENERIC", - "choices": [], - "timeCreated": created_time, - "usage": { - "promptTokens": 5, - "completionTokens": 8, - "totalTokens": 13, - # missing completionTokensDetails and promptTokensDetails - }, - }, + # Case 1: fields completely missing + data_missing = { + "promptTokens": 10, + "completionTokens": 5, + "totalTokens": 15, + # no completionTokensDetails or promptTokensDetails } - - response_missing = httpx.Response(status_code=200, json=mock_oci_response_missing) - result_missing = config.transform_response( - model=TEST_MODEL_NAME, - raw_response=response_missing, - model_response=ModelResponse(), - logging_obj={}, - request_data={}, - messages=[], - optional_params={}, - litellm_params={}, - encoding={}, - ) - - usage_missing = result_missing.usage # type: ignore + usage_missing = OCIResponseUsage(**data_missing) assert usage_missing.completionTokensDetails is None assert usage_missing.promptTokensDetails is None + assert usage_missing.promptTokens == 10 + assert usage_missing.completionTokens == 5 + assert usage_missing.totalTokens == 15 - # Case 2: usage fields explicitly null - mock_oci_response_null = { - "modelId": TEST_MODEL_NAME, - "modelVersion": "1.0", - "chatResponse": { - "apiFormat": "GENERIC", - "choices": [], - "timeCreated": created_time, - "usage": { - "promptTokens": 5, - "completionTokens": 8, - "totalTokens": 13, - "completionTokensDetails": None, - "promptTokensDetails": None, - }, - }, + # Case 2: fields explicitly null + data_null = { + "promptTokens": 12, + "completionTokens": 8, + "totalTokens": 20, + "completionTokensDetails": None, + "promptTokensDetails": None, } - - response_null = httpx.Response(status_code=200, json=mock_oci_response_null) - result_null = config.transform_response( - model=TEST_MODEL_NAME, - raw_response=response_null, - model_response=ModelResponse(), - logging_obj={}, - request_data={}, - messages=[], - optional_params={}, - litellm_params={}, - encoding={}, - ) - - usage_null = result_null.usage # type: ignore + usage_null = OCIResponseUsage(**data_null) assert usage_null.completionTokensDetails is None - assert usage_null.promptTokensDetails is None \ No newline at end of file + assert usage_null.promptTokensDetails is None + assert usage_null.promptTokens == 12 + assert usage_null.completionTokens == 8 + assert usage_null.totalTokens == 20 + + # Case 3: fields present with values + data_present = { + "promptTokens": 7, + "completionTokens": 3, + "totalTokens": 10, + "completionTokensDetails": {"acceptedPredictionTokens": 2, "reasoningTokens": 1}, + "promptTokensDetails": {"cachedTokens": 4}, + } + usage_present = OCIResponseUsage(**data_present) + assert usage_present.completionTokensDetails.acceptedPredictionTokens == 2 + assert usage_present.completionTokensDetails.reasoningTokens == 1 + assert usage_present.promptTokensDetails.cachedTokens == 4 + assert usage_present.promptTokens == 7 + assert usage_present.completionTokens == 3 + assert usage_present.totalTokens == 10 From 07d9d14b49183c334e3d6ba720a92a904d72a701 Mon Sep 17 00:00:00 2001 From: gotsysdba Date: Sat, 30 Aug 2025 22:02:00 +0100 Subject: [PATCH 3/3] Update OCIResposeClass --- litellm/types/llms/oci.py | 2 +- .../oci/chat/test_oci_chat_transformation.py | 56 ++----------------- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/litellm/types/llms/oci.py b/litellm/types/llms/oci.py index e3ea07463e..75d13192c5 100644 --- a/litellm/types/llms/oci.py +++ b/litellm/types/llms/oci.py @@ -129,7 +129,7 @@ class OCIPromptTokensDetails(BaseModel): class OCIResponseUsage(BaseModel): """Token usage in the OCI response.""" - + promptTokens: int completionTokens: int totalTokens: int diff --git a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py index 3151bed109..9ff93dfc1a 100644 --- a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py +++ b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py @@ -213,6 +213,10 @@ class TestOCIChatConfig: assert result.usage.prompt_tokens == 10 # type: ignore assert result.usage.completion_tokens == 20 # type: ignore assert result.usage.total_tokens == 30 # type: ignore + # These are not handled in the transformer, TBH no idea why they are here + # but, for now, they seem to be always None + assert result.usage.completion_tokens_details is None + assert result.usage.prompt_tokens_details is None def test_transform_response_with_tool_calls(self): """ @@ -305,55 +309,3 @@ class TestOCIChatConfig: assert usage.prompt_tokens == 10 # type: ignore assert usage.completion_tokens == 20 # type: ignore assert usage.total_tokens == 30 # type: ignore - -def test_oci_response_usage_handles_missing_and_null_fields(self): - """ - Test that OCIResponseUsage parses correctly when - completionTokensDetails and promptTokensDetails are missing or null. - """ - from litellm.llms.oci.chat.transformation import OCIResponseUsage - - # Case 1: fields completely missing - data_missing = { - "promptTokens": 10, - "completionTokens": 5, - "totalTokens": 15, - # no completionTokensDetails or promptTokensDetails - } - usage_missing = OCIResponseUsage(**data_missing) - assert usage_missing.completionTokensDetails is None - assert usage_missing.promptTokensDetails is None - assert usage_missing.promptTokens == 10 - assert usage_missing.completionTokens == 5 - assert usage_missing.totalTokens == 15 - - # Case 2: fields explicitly null - data_null = { - "promptTokens": 12, - "completionTokens": 8, - "totalTokens": 20, - "completionTokensDetails": None, - "promptTokensDetails": None, - } - usage_null = OCIResponseUsage(**data_null) - assert usage_null.completionTokensDetails is None - assert usage_null.promptTokensDetails is None - assert usage_null.promptTokens == 12 - assert usage_null.completionTokens == 8 - assert usage_null.totalTokens == 20 - - # Case 3: fields present with values - data_present = { - "promptTokens": 7, - "completionTokens": 3, - "totalTokens": 10, - "completionTokensDetails": {"acceptedPredictionTokens": 2, "reasoningTokens": 1}, - "promptTokensDetails": {"cachedTokens": 4}, - } - usage_present = OCIResponseUsage(**data_present) - assert usage_present.completionTokensDetails.acceptedPredictionTokens == 2 - assert usage_present.completionTokensDetails.reasoningTokens == 1 - assert usage_present.promptTokensDetails.cachedTokens == 4 - assert usage_present.promptTokens == 7 - assert usage_present.completionTokens == 3 - assert usage_present.totalTokens == 10