mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-09 12:24:21 +00:00
Merge pull request #15441 from BerriAI/litellm_dev_10_10_2025_p3
GPT-5 return reasoning content via `/chat/completions` + GPT-5-Codex working on Claude Code
This commit is contained in:
@@ -339,6 +339,72 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
||||
| fine tuned `gpt-3.5-turbo-1106` | `response = completion(model="ft:gpt-3.5-turbo-1106", messages=messages)` |
|
||||
| fine tuned `gpt-3.5-turbo-0613` | `response = completion(model="ft:gpt-3.5-turbo-0613", messages=messages)` |
|
||||
|
||||
## Getting Reasoning Content in `/chat/completions`
|
||||
|
||||
GPT-5 models return reasoning content when called via the Responses API. You can call these models via the `/chat/completions` endpoint by using the `openai/responses/` prefix.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
```python
|
||||
response = litellm.completion(
|
||||
model="openai/responses/gpt-5-mini", # tells litellm to call the model via the Responses API
|
||||
messages=[{"role": "user", "content": "What is the capital of France?"}],
|
||||
reasoning_effort="low",
|
||||
)
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-d '{
|
||||
"model": "openai/responses/gpt-5-mini",
|
||||
"messages": [{"role": "user", "content": "What is the capital of France?"}],
|
||||
"reasoning_effort": "low"
|
||||
}'
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Expected Response:
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-6382a222-43c9-40c4-856b-22e105d88075",
|
||||
"created": 1760146746,
|
||||
"model": "gpt-5-mini",
|
||||
"object": "chat.completion",
|
||||
"system_fingerprint": null,
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {
|
||||
"content": "Paris",
|
||||
"role": "assistant",
|
||||
"tool_calls": null,
|
||||
"function_call": null,
|
||||
"reasoning_content": "**Identifying the capital**\n\nThe user wants me to think of the capital of France and write it down. That's pretty straightforward: it's Paris. There aren't any safety issues to consider here. I think it would be best to keep it concise, so maybe just \"Paris\" would suffice. I feel confident that I should just stick to that without adding anything else. So, let's write it down!",
|
||||
"provider_specific_fields": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"completion_tokens": 7,
|
||||
"prompt_tokens": 18,
|
||||
"total_tokens": 25,
|
||||
"completion_tokens_details": null,
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0,
|
||||
"text_tokens": null,
|
||||
"image_tokens": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## OpenAI Chat Completion to Responses API Bridge
|
||||
|
||||
|
||||
@@ -18,13 +18,15 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from openai.types.responses.tool_param import FunctionToolParam
|
||||
|
||||
from litellm import ModelResponse
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.bridges.completion_transformation import (
|
||||
CompletionTransformationBridge,
|
||||
)
|
||||
from litellm.types.llms.openai import Reasoning
|
||||
from litellm.types.llms.openai import ChatCompletionToolParamFunctionChunk, Reasoning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openai.types.responses import ResponseInputImageParam
|
||||
@@ -242,6 +244,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
if value is not None:
|
||||
if key == "instructions" and instructions:
|
||||
request_data["instructions"] = instructions
|
||||
elif key == "stream_options" and isinstance(value, dict):
|
||||
request_data["stream_options"] = value.get("include_obfuscation")
|
||||
elif key == "user": # string can't be longer than 64 characters
|
||||
if isinstance(value, str) and len(value) <= 64:
|
||||
request_data["user"] = value
|
||||
else:
|
||||
request_data[key] = value
|
||||
|
||||
@@ -262,7 +269,6 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
json_mode: Optional[bool] = None,
|
||||
) -> "ModelResponse":
|
||||
"""Transform Responses API response to chat completion response"""
|
||||
|
||||
from openai.types.responses import (
|
||||
ResponseFunctionToolCall,
|
||||
ResponseOutputMessage,
|
||||
@@ -281,19 +287,35 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
|
||||
choices: List[Choices] = []
|
||||
index = 0
|
||||
|
||||
reasoning_content: Optional[str] = None
|
||||
|
||||
for item in raw_response.output:
|
||||
|
||||
if isinstance(item, ResponseReasoningItem):
|
||||
pass # ignore for now.
|
||||
|
||||
for content in item.summary:
|
||||
response_text = getattr(content, "text", "")
|
||||
reasoning_content = response_text if response_text else ""
|
||||
|
||||
elif isinstance(item, ResponseOutputMessage):
|
||||
for content in item.content:
|
||||
response_text = getattr(content, "text", "")
|
||||
msg = Message(
|
||||
role=item.role, content=response_text if response_text else ""
|
||||
role=item.role,
|
||||
content=response_text if response_text else "",
|
||||
reasoning_content=reasoning_content,
|
||||
)
|
||||
|
||||
choices.append(
|
||||
Choices(message=msg, finish_reason="stop", index=index)
|
||||
Choices(
|
||||
message=msg,
|
||||
finish_reason="stop",
|
||||
index=index,
|
||||
)
|
||||
)
|
||||
|
||||
reasoning_content = None # flush reasoning content
|
||||
index += 1
|
||||
elif isinstance(item, ResponseFunctionToolCall):
|
||||
msg = Message(
|
||||
@@ -308,11 +330,13 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
"type": "function",
|
||||
}
|
||||
],
|
||||
reasoning_content=reasoning_content,
|
||||
)
|
||||
|
||||
choices.append(
|
||||
Choices(message=msg, finish_reason="tool_calls", index=index)
|
||||
)
|
||||
reasoning_content = None # flush reasoning content
|
||||
index += 1
|
||||
elif isinstance(item, dict):
|
||||
# Handle raw dict responses (e.g., from GPT-5 Codex)
|
||||
@@ -493,9 +517,25 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
self, tools: List[Dict[str, Any]]
|
||||
) -> List["ALL_RESPONSES_API_TOOL_PARAMS"]:
|
||||
"""Convert chat completion tools to responses API tools format"""
|
||||
responses_tools = []
|
||||
responses_tools: List["ALL_RESPONSES_API_TOOL_PARAMS"] = []
|
||||
for tool in tools:
|
||||
responses_tools.append(tool)
|
||||
# convert function tool from chat completion to responses API format
|
||||
if tool.get("type") == "function":
|
||||
function_tool = cast(
|
||||
ChatCompletionToolParamFunctionChunk, tool.get("function")
|
||||
)
|
||||
responses_tools.append(
|
||||
FunctionToolParam(
|
||||
name=function_tool["name"],
|
||||
parameters=function_tool.get("parameters"),
|
||||
strict=function_tool.get("strict"),
|
||||
type="function",
|
||||
description=function_tool.get("description"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
responses_tools.append(tool) # type: ignore
|
||||
|
||||
return cast(List["ALL_RESPONSES_API_TOOL_PARAMS"], responses_tools)
|
||||
|
||||
def _map_reasoning_effort(self, reasoning_effort: str) -> Optional[Reasoning]:
|
||||
|
||||
@@ -133,7 +133,6 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
||||
**kwargs,
|
||||
) -> Union[AnthropicMessagesResponse, AsyncIterator]:
|
||||
"""Handle non-Anthropic models asynchronously using the adapter"""
|
||||
|
||||
completion_kwargs = (
|
||||
LiteLLMMessagesToCompletionTransformationHandler._prepare_completion_kwargs(
|
||||
max_tokens=max_tokens,
|
||||
|
||||
@@ -13,13 +13,13 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
import httpx # type: ignore
|
||||
|
||||
import litellm
|
||||
import litellm.litellm_core_utils
|
||||
import litellm.types
|
||||
import litellm.types.utils
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.realtime_streaming import RealTimeStreaming
|
||||
from litellm.llms.base_llm.anthropic_messages.transformation import (
|
||||
BaseAnthropicMessagesConfig,
|
||||
@@ -239,7 +239,7 @@ class BaseLLMHTTPHandler:
|
||||
json_mode: bool = False,
|
||||
signed_json_body: Optional[bytes] = None,
|
||||
shared_session: Optional["ClientSession"] = None,
|
||||
):
|
||||
):
|
||||
if client is None:
|
||||
verbose_logger.debug(
|
||||
f"Creating HTTP client with shared_session: {id(shared_session) if shared_session else None}"
|
||||
@@ -1533,6 +1533,7 @@ class BaseLLMHTTPHandler:
|
||||
data=data,
|
||||
fake_stream=fake_stream,
|
||||
)
|
||||
|
||||
response = sync_httpx_client.post(
|
||||
url=api_base,
|
||||
headers=headers,
|
||||
|
||||
@@ -161,6 +161,10 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||
) -> ResponsesAPIResponse:
|
||||
"""No transform applied since outputs are in OpenAI spec already"""
|
||||
try:
|
||||
logging_obj.post_call(
|
||||
original_response=raw_response.text,
|
||||
additional_args={"complete_input_dict": {}},
|
||||
)
|
||||
raw_response_json = raw_response.json()
|
||||
raw_response_json["created_at"] = _safe_convert_created_field(
|
||||
raw_response_json["created_at"]
|
||||
@@ -169,7 +173,13 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||
raise OpenAIError(
|
||||
message=raw_response.text, status_code=raw_response.status_code
|
||||
)
|
||||
return ResponsesAPIResponse(**raw_response_json)
|
||||
try:
|
||||
return ResponsesAPIResponse(**raw_response_json)
|
||||
except Exception:
|
||||
verbose_logger.debug(
|
||||
f"Error constructing ResponsesAPIResponse: {raw_response_json}, using model_construct"
|
||||
)
|
||||
return ResponsesAPIResponse.model_construct(**raw_response_json)
|
||||
|
||||
def validate_environment(
|
||||
self, headers: dict, model: str, litellm_params: Optional[GenericLiteLLMParams]
|
||||
|
||||
@@ -13074,34 +13074,6 @@
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gpt-5-codex": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
"input_cost_per_token": 1.25e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 400000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-05,
|
||||
"supported_endpoints": [
|
||||
"/v1/responses"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"gpt-5-2025-08-07": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
"cache_read_input_token_cost_flex": 6.25e-08,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,29 +1,4 @@
|
||||
model_list:
|
||||
- model_name: gpt-5-mini
|
||||
- model_name: gpt-5-codex
|
||||
litellm_params:
|
||||
model: openai/gpt-4o-mini
|
||||
api_base: "https://webhook.site/2f385e05-00aa-402b-86d1-efc9261471a5"
|
||||
api_key: dummy
|
||||
- model_name: "byok-wildcard/*"
|
||||
litellm_params:
|
||||
model: openai/*
|
||||
- model_name: xai-grok-3
|
||||
litellm_params:
|
||||
model: xai/grok-3
|
||||
- model_name: hosted_vllm/whisper-v3
|
||||
litellm_params:
|
||||
model: hosted_vllm/whisper-v3
|
||||
api_base: "https://webhook.site/2f385e05-00aa-402b-86d1-efc9261471a5"
|
||||
api_key: dummy
|
||||
|
||||
mcp_servers:
|
||||
my_api_mcp:
|
||||
url: "http://0.0.0.0:8090"
|
||||
spec_path: "/Users/krrishdholakia/Documents/temp_py_folder/example_openapi.json"
|
||||
auth_type: none
|
||||
allowed_tools: ["getpetbyid", "my_api_mcp-findpetsbystatus"]
|
||||
|
||||
|
||||
litellm_settings:
|
||||
callbacks: ["prometheus"]
|
||||
custom_prometheus_metadata_labels: ["metadata.initiative", "metadata.business-unit"]
|
||||
model: gpt-5-codex
|
||||
@@ -13074,34 +13074,6 @@
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gpt-5-codex": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
"input_cost_per_token": 1.25e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 400000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1e-05,
|
||||
"supported_endpoints": [
|
||||
"/v1/responses"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supports_function_calling": true,
|
||||
"supports_native_streaming": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"gpt-5-2025-08-07": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
"cache_read_input_token_cost_flex": 6.25e-08,
|
||||
|
||||
@@ -302,20 +302,16 @@ def test_gemini_2_5_flash_image_preview():
|
||||
mock_response = ImageResponse()
|
||||
mock_response.data = [ImageObject(b64_json="test_base64_data", url=None)]
|
||||
|
||||
with patch("litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post") as mock_post:
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post"
|
||||
) as mock_post:
|
||||
# Mock successful HTTP response
|
||||
mock_http_response = MagicMock()
|
||||
mock_http_response.json.return_value = {
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"inlineData": {
|
||||
"data": "test_base64_image_data"
|
||||
}
|
||||
}
|
||||
]
|
||||
"parts": [{"inlineData": {"data": "test_base64_image_data"}}]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -327,33 +323,38 @@ def test_gemini_2_5_flash_image_preview():
|
||||
response = litellm.image_generation(
|
||||
model="gemini/gemini-2.5-flash-image-preview",
|
||||
prompt="Generate a simple test image",
|
||||
api_key="test_api_key"
|
||||
api_key="test_api_key",
|
||||
)
|
||||
|
||||
# Validate response structure
|
||||
assert response is not None
|
||||
assert hasattr(response, 'data')
|
||||
assert hasattr(response, "data")
|
||||
assert response.data is not None
|
||||
assert len(response.data) > 0
|
||||
|
||||
# Validate the correct endpoint was called
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
called_url = call_args[0][0] if call_args[0] else call_args.kwargs.get('url', '')
|
||||
called_url = (
|
||||
call_args[0][0] if call_args[0] else call_args.kwargs.get("url", "")
|
||||
)
|
||||
|
||||
# Verify it uses generateContent endpoint for gemini-2.5-flash-image-preview (not predict)
|
||||
assert ":generateContent" in called_url
|
||||
assert "gemini-2.5-flash-image-preview" in called_url
|
||||
|
||||
# Verify request format is Gemini format (not Imagen)
|
||||
request_data = call_args.kwargs.get('json', {})
|
||||
request_data = call_args.kwargs.get("json", {})
|
||||
assert "contents" in request_data
|
||||
assert "parts" in request_data["contents"][0]
|
||||
|
||||
# Verify response_modalities is set correctly for image generation
|
||||
assert "generationConfig" in request_data
|
||||
assert "response_modalities" in request_data["generationConfig"]
|
||||
assert request_data["generationConfig"]["response_modalities"] == ["IMAGE", "TEXT"]
|
||||
assert request_data["generationConfig"]["response_modalities"] == [
|
||||
"IMAGE",
|
||||
"TEXT",
|
||||
]
|
||||
|
||||
|
||||
def test_gemini_imagen_models_use_predict_endpoint():
|
||||
@@ -363,15 +364,13 @@ def test_gemini_imagen_models_use_predict_endpoint():
|
||||
from unittest.mock import patch, MagicMock
|
||||
from litellm.types.utils import ImageResponse, ImageObject
|
||||
|
||||
with patch("litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post") as mock_post:
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post"
|
||||
) as mock_post:
|
||||
# Mock successful HTTP response for Imagen
|
||||
mock_http_response = MagicMock()
|
||||
mock_http_response.json.return_value = {
|
||||
"predictions": [
|
||||
{
|
||||
"bytesBase64Encoded": "test_base64_image_data"
|
||||
}
|
||||
]
|
||||
"predictions": [{"bytesBase64Encoded": "test_base64_image_data"}]
|
||||
}
|
||||
mock_http_response.status_code = 200
|
||||
mock_post.return_value = mock_http_response
|
||||
@@ -380,17 +379,19 @@ def test_gemini_imagen_models_use_predict_endpoint():
|
||||
response = litellm.image_generation(
|
||||
model="gemini/imagen-3.0-generate-001",
|
||||
prompt="Generate a simple test image",
|
||||
api_key="test_api_key"
|
||||
api_key="test_api_key",
|
||||
)
|
||||
|
||||
# Validate response structure
|
||||
assert response is not None
|
||||
assert hasattr(response, 'data')
|
||||
assert hasattr(response, "data")
|
||||
|
||||
# Validate the correct endpoint was called for Imagen models
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
called_url = call_args[0][0] if call_args[0] else call_args.kwargs.get('url', '')
|
||||
called_url = (
|
||||
call_args[0][0] if call_args[0] else call_args.kwargs.get("url", "")
|
||||
)
|
||||
|
||||
# Verify Imagen models use predict endpoint (not generateContent)
|
||||
assert ":predict" in called_url
|
||||
@@ -398,7 +399,7 @@ def test_gemini_imagen_models_use_predict_endpoint():
|
||||
assert ":generateContent" not in called_url
|
||||
|
||||
# Verify request format is Imagen format (not Gemini)
|
||||
request_data = call_args.kwargs.get('json', {})
|
||||
request_data = call_args.kwargs.get("json", {})
|
||||
assert "instances" in request_data
|
||||
assert "parameters" in request_data
|
||||
|
||||
@@ -997,9 +998,7 @@ def test_gemini_exception_message_format():
|
||||
|
||||
# Create a mock exception that simulates a Gemini API error
|
||||
mock_exception = httpx.HTTPStatusError(
|
||||
message="Bad Request",
|
||||
request=Mock(),
|
||||
response=mock_response
|
||||
message="Bad Request", request=Mock(), response=mock_response
|
||||
)
|
||||
mock_exception.response = mock_response
|
||||
mock_exception.status_code = 400
|
||||
@@ -1011,7 +1010,7 @@ def test_gemini_exception_message_format():
|
||||
original_exception=mock_exception,
|
||||
custom_llm_provider="gemini",
|
||||
completion_kwargs={},
|
||||
extra_kwargs={}
|
||||
extra_kwargs={},
|
||||
)
|
||||
# Should not reach here - exception should be raised
|
||||
assert False, "Expected BadRequestError to be raised"
|
||||
@@ -1026,22 +1025,25 @@ def test_gemini_exception_message_format():
|
||||
f"Expected 'GeminiException' in error message, got: {error_message}. "
|
||||
f"This test should fail before the fix is implemented."
|
||||
)
|
||||
assert "VertexAIException" not in error_message, (
|
||||
f"Should not contain 'VertexAIException' in error message, got: {error_message}"
|
||||
)
|
||||
assert (
|
||||
"VertexAIException" not in error_message
|
||||
), f"Should not contain 'VertexAIException' in error message, got: {error_message}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status_code,expected_exception", [
|
||||
(400, "BadRequestError"),
|
||||
(401, "AuthenticationError"),
|
||||
(403, "PermissionDeniedError"),
|
||||
(404, "NotFoundError"),
|
||||
(408, "Timeout"),
|
||||
(429, "RateLimitError"),
|
||||
(500, "InternalServerError"),
|
||||
(502, "APIConnectionError"),
|
||||
(503, "ServiceUnavailableError"),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"status_code,expected_exception",
|
||||
[
|
||||
(400, "BadRequestError"),
|
||||
(401, "AuthenticationError"),
|
||||
(403, "PermissionDeniedError"),
|
||||
(404, "NotFoundError"),
|
||||
(408, "Timeout"),
|
||||
(429, "RateLimitError"),
|
||||
(500, "InternalServerError"),
|
||||
(502, "APIConnectionError"),
|
||||
(503, "ServiceUnavailableError"),
|
||||
],
|
||||
)
|
||||
def l(status_code, expected_exception):
|
||||
"""
|
||||
Test comprehensive Gemini error handling for all HTTP status codes.
|
||||
@@ -1053,8 +1055,15 @@ def l(status_code, expected_exception):
|
||||
from unittest.mock import Mock
|
||||
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
|
||||
from litellm.exceptions import (
|
||||
BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError,
|
||||
Timeout, RateLimitError, InternalServerError, APIConnectionError, ServiceUnavailableError
|
||||
BadRequestError,
|
||||
AuthenticationError,
|
||||
PermissionDeniedError,
|
||||
NotFoundError,
|
||||
Timeout,
|
||||
RateLimitError,
|
||||
InternalServerError,
|
||||
APIConnectionError,
|
||||
ServiceUnavailableError,
|
||||
)
|
||||
|
||||
# Mock the appropriate error response
|
||||
@@ -1065,9 +1074,7 @@ def l(status_code, expected_exception):
|
||||
|
||||
# Create a mock exception
|
||||
mock_exception = httpx.HTTPStatusError(
|
||||
message=f"HTTP {status_code}",
|
||||
request=Mock(),
|
||||
response=mock_response
|
||||
message=f"HTTP {status_code}", request=Mock(), response=mock_response
|
||||
)
|
||||
mock_exception.response = mock_response
|
||||
mock_exception.status_code = status_code
|
||||
@@ -1081,9 +1088,11 @@ def l(status_code, expected_exception):
|
||||
original_exception=mock_exception,
|
||||
custom_llm_provider="gemini",
|
||||
completion_kwargs={},
|
||||
extra_kwargs={}
|
||||
extra_kwargs={},
|
||||
)
|
||||
assert False, f"Expected {expected_exception} to be raised for status {status_code}"
|
||||
assert (
|
||||
False
|
||||
), f"Expected {expected_exception} to be raised for status {status_code}"
|
||||
except Exception as e:
|
||||
# Verify the correct exception type is raised
|
||||
exception_classes = {
|
||||
@@ -1098,13 +1107,25 @@ def l(status_code, expected_exception):
|
||||
"ServiceUnavailableError": ServiceUnavailableError,
|
||||
}
|
||||
expected_class = exception_classes[expected_exception]
|
||||
assert isinstance(e, expected_class), f"Expected {expected_exception}, got {type(e).__name__}"
|
||||
assert isinstance(
|
||||
e, expected_class
|
||||
), f"Expected {expected_exception}, got {type(e).__name__}"
|
||||
|
||||
# Verify the error message contains GeminiException
|
||||
error_message = str(e)
|
||||
assert "GeminiException" in error_message, (
|
||||
f"Expected 'GeminiException' in error message for status {status_code}, got: {error_message}"
|
||||
)
|
||||
assert "VertexAIException" not in error_message, (
|
||||
f"Should not contain 'VertexAIException' for status {status_code}, got: {error_message}"
|
||||
)
|
||||
assert (
|
||||
"GeminiException" in error_message
|
||||
), f"Expected 'GeminiException' in error message for status {status_code}, got: {error_message}"
|
||||
assert (
|
||||
"VertexAIException" not in error_message
|
||||
), f"Should not contain 'VertexAIException' for status {status_code}, got: {error_message}"
|
||||
|
||||
|
||||
def test_gemini_embedding():
|
||||
litellm._turn_on_debug()
|
||||
response = litellm.embedding(
|
||||
model="gemini/gemini-embedding-001",
|
||||
input="Hello, world!",
|
||||
)
|
||||
print("response: ", response)
|
||||
assert response is not None
|
||||
|
||||
File diff suppressed because one or more lines are too long
+189
@@ -84,3 +84,192 @@ def test_openai_responses_chunk_parser_reasoning_summary():
|
||||
assert delta.reasoning_content == "**Compar"
|
||||
assert delta.tool_calls is None
|
||||
assert delta.function_call is None
|
||||
|
||||
|
||||
def test_transform_response_with_reasoning_and_output():
|
||||
"""Test transform_response handles ResponsesAPIResponse with reasoning items and output messages."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
from openai.types.responses.response_reasoning_item import (
|
||||
ResponseReasoningItem,
|
||||
Summary,
|
||||
)
|
||||
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
from litellm.types.llms.openai import (
|
||||
InputTokensDetails,
|
||||
OutputTokensDetails,
|
||||
ResponseAPIUsage,
|
||||
ResponsesAPIResponse,
|
||||
)
|
||||
from litellm.types.utils import ModelResponse, Usage
|
||||
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
# Create the reasoning item with summary
|
||||
reasoning_summary = Summary(
|
||||
text="**Creating a poem**\n\nThe user wants a poem without constraints, which is great! I need to focus on keeping it original and evocative.",
|
||||
type="summary_text",
|
||||
)
|
||||
reasoning_item = ResponseReasoningItem(
|
||||
id="rs_04c8021b8b3188a00068e9ae08c2d8819d82268b129351a979",
|
||||
summary=[reasoning_summary],
|
||||
type="reasoning",
|
||||
content=None,
|
||||
encrypted_content=None,
|
||||
status=None,
|
||||
)
|
||||
|
||||
# Create the output message with the poem
|
||||
poem_text = """I found a pocket of evening
|
||||
hidden behind the gutters of the day —
|
||||
a small, folded sky of blue
|
||||
that hummed like a hush.
|
||||
|
||||
The streetlight rehearsed its first apology,
|
||||
slowly pulling down the curtain
|
||||
on the city's impatient laughter.
|
||||
Windows blinked awake like tired eyes,
|
||||
and the air remembered rain it once promised.
|
||||
|
||||
You walked by with a map of quiet in your hands,
|
||||
tracing routes that led away from all the clocks.
|
||||
For a moment the coffee shop's bell
|
||||
tied our minutes together — bright and accidental —
|
||||
and the world refined itself to the size of that bell's sound.
|
||||
|
||||
We did not name the solitude; we sipped it.
|
||||
You left a warmth on the bench like a small sun,
|
||||
and night stitched the rest into blue and shadow.
|
||||
Tomorrow will bring its petitions and promises,
|
||||
but for now the city breathes slow and wide,
|
||||
and I learn to carry this small calm home."""
|
||||
|
||||
output_text = ResponseOutputText(
|
||||
annotations=[], text=poem_text, type="output_text", logprobs=[]
|
||||
)
|
||||
output_message = ResponseOutputMessage(
|
||||
id="msg_04c8021b8b3188a00068e9ae0b92f4819dac64d85b4abb67ec",
|
||||
content=[output_text],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
type="message",
|
||||
)
|
||||
|
||||
# Create usage information
|
||||
usage = ResponseAPIUsage(
|
||||
input_tokens=16,
|
||||
input_tokens_details=InputTokensDetails(
|
||||
audio_tokens=None, cached_tokens=0, text_tokens=None
|
||||
),
|
||||
output_tokens=195,
|
||||
output_tokens_details=OutputTokensDetails(reasoning_tokens=0, text_tokens=None),
|
||||
total_tokens=211,
|
||||
cost=None,
|
||||
)
|
||||
|
||||
# Create the full ResponsesAPIResponse
|
||||
raw_response = ResponsesAPIResponse(
|
||||
id="resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDpOb25lO3Jlc3BvbnNlX2lkOnJlc3BfMDRjODAyMWI4YjMxODhhMDAwNjhlOWFlMDgyYmZjODE5ZDhmNDk0OTI5MWMzMzM4YTc=",
|
||||
created_at=1760144904,
|
||||
error=None,
|
||||
incomplete_details=None,
|
||||
instructions=None,
|
||||
metadata={},
|
||||
model="gpt-5-mini-2025-08-07",
|
||||
object="response",
|
||||
output=[reasoning_item, output_message],
|
||||
parallel_tool_calls=True,
|
||||
temperature=1.0,
|
||||
tool_choice="auto",
|
||||
tools=[],
|
||||
top_p=1.0,
|
||||
max_output_tokens=None,
|
||||
previous_response_id=None,
|
||||
reasoning={"effort": "low", "summary": "detailed"},
|
||||
status="completed",
|
||||
text={"format": {"type": "text"}, "verbosity": "medium"},
|
||||
truncation="disabled",
|
||||
usage=usage,
|
||||
user=None,
|
||||
store=True,
|
||||
background=False,
|
||||
billing={"payer": "developer"},
|
||||
max_tool_calls=None,
|
||||
prompt_cache_key=None,
|
||||
safety_identifier=None,
|
||||
service_tier="default",
|
||||
top_logprobs=0,
|
||||
)
|
||||
|
||||
# Create empty model_response
|
||||
model_response = ModelResponse(
|
||||
id="chatcmpl-42e863c4-7a31-4229-84f3-4c3a6eeb7610",
|
||||
created=1760144904,
|
||||
model=None,
|
||||
object="chat.completion",
|
||||
system_fingerprint=None,
|
||||
choices=[],
|
||||
usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
|
||||
)
|
||||
|
||||
# Create mock objects for required parameters
|
||||
logging_obj = Mock()
|
||||
messages = [{"role": "user", "content": "Think of a poem, and then write it."}]
|
||||
request_data = {"model": "gpt-5-mini"}
|
||||
optional_params = {"reasoning_effort": "low", "extra_body": {}}
|
||||
litellm_params = {"acompletion": False, "api_key": None}
|
||||
encoding = Mock()
|
||||
|
||||
# Call transform_response
|
||||
result = handler.transform_response(
|
||||
model="gpt-5-mini",
|
||||
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=None,
|
||||
json_mode=None,
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result.model == "gpt-5-mini"
|
||||
assert len(result.choices) == 1
|
||||
|
||||
# Check the choice
|
||||
choice = result.choices[0]
|
||||
assert choice.finish_reason == "stop"
|
||||
assert choice.index == 0
|
||||
assert choice.message.role == "assistant"
|
||||
assert choice.message.content == poem_text
|
||||
|
||||
# Check usage
|
||||
assert result.usage.prompt_tokens == 16
|
||||
assert result.usage.completion_tokens == 195
|
||||
assert result.usage.total_tokens == 211
|
||||
|
||||
# Check reasoning content
|
||||
assert choice.message.reasoning_content == reasoning_summary.text
|
||||
|
||||
print("✓ transform_response correctly handled reasoning items and output messages")
|
||||
|
||||
|
||||
def test_convert_tools_to_responses_format():
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
tools = [{"type": "function", "function": {"name": "test", "arguments": "test"}}]
|
||||
|
||||
result = handler._convert_tools_to_responses_format(tools)
|
||||
|
||||
assert result[0]["name"] == "test"
|
||||
|
||||
Reference in New Issue
Block a user