mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
address review: normalize timeout consistently, fix sync converse path, add tests
Made-with: Cursor
This commit is contained in:
parent
bd0e92926a
commit
24b768e271
5 changed files with 403 additions and 567 deletions
|
|
@ -42,7 +42,9 @@ def make_sync_call(
|
|||
_params: dict = {}
|
||||
if timeout is not None:
|
||||
_params["timeout"] = timeout
|
||||
client = _get_httpx_client(params=_params if _params else None)
|
||||
client = _get_httpx_client(
|
||||
params=_params if _params else None
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
api_base,
|
||||
|
|
@ -78,9 +80,7 @@ def make_sync_call(
|
|||
)
|
||||
else:
|
||||
decoder = AWSEventStreamDecoder(model=model, json_mode=json_mode)
|
||||
completion_stream = decoder.iter_bytes(
|
||||
response.iter_bytes(chunk_size=stream_chunk_size)
|
||||
)
|
||||
completion_stream = decoder.iter_bytes(response.iter_bytes(chunk_size=stream_chunk_size))
|
||||
|
||||
# LOGGING
|
||||
logging_obj.post_call(
|
||||
|
|
@ -134,7 +134,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
endpoint_url=api_base,
|
||||
data=data,
|
||||
headers=headers,
|
||||
api_key=api_key,
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
## LOGGING
|
||||
|
|
@ -195,7 +195,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
headers=headers,
|
||||
)
|
||||
data = json.dumps(request_data)
|
||||
|
||||
|
||||
prepped = self.get_request_headers(
|
||||
credentials=credentials,
|
||||
aws_region_name=litellm_params.get("aws_region_name") or "us-west-2",
|
||||
|
|
@ -203,7 +203,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
endpoint_url=api_base,
|
||||
data=data,
|
||||
headers=headers,
|
||||
api_key=api_key,
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
## LOGGING
|
||||
|
|
@ -289,7 +289,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
_stripped = _model_for_id
|
||||
for rp in ["bedrock/converse/", "bedrock/", "converse/"]:
|
||||
if _stripped.startswith(rp):
|
||||
_stripped = _stripped[len(rp) :]
|
||||
_stripped = _stripped[len(rp):]
|
||||
break
|
||||
# Strip embedded region prefix (e.g. "bedrock/us-east-1/model" -> "model")
|
||||
# and capture it so it can be used as aws_region_name below.
|
||||
|
|
@ -305,10 +305,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
break
|
||||
modelId = self.encode_model_id(model_id=_model_for_id)
|
||||
# Inject region extracted from model path so _get_aws_region_name picks it up
|
||||
if (
|
||||
_region_from_model is not None
|
||||
and "aws_region_name" not in optional_params
|
||||
):
|
||||
if _region_from_model is not None and "aws_region_name" not in optional_params:
|
||||
optional_params["aws_region_name"] = _region_from_model
|
||||
|
||||
fake_stream = litellm.AmazonConverseConfig().should_fake_stream(
|
||||
|
|
@ -318,6 +315,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
custom_llm_provider="bedrock",
|
||||
)
|
||||
|
||||
|
||||
### SET REGION NAME ###
|
||||
aws_region_name = self._get_aws_region_name(
|
||||
optional_params=optional_params,
|
||||
|
|
@ -375,7 +373,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
headers = {"Content-Type": "application/json"}
|
||||
if extra_headers is not None:
|
||||
headers = {"Content-Type": "application/json", **extra_headers}
|
||||
|
||||
|
||||
# Filter beta headers in HTTP headers before making the request
|
||||
headers = update_headers_with_filtered_beta(
|
||||
headers=headers, provider="bedrock_converse"
|
||||
|
|
@ -421,7 +419,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
timeout=timeout,
|
||||
client=client,
|
||||
credentials=credentials,
|
||||
api_key=api_key,
|
||||
api_key=api_key
|
||||
) # type: ignore
|
||||
|
||||
## TRANSFORMATION ##
|
||||
|
|
@ -434,7 +432,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
headers=extra_headers,
|
||||
)
|
||||
data = json.dumps(_data)
|
||||
|
||||
|
||||
prepped = self.get_request_headers(
|
||||
credentials=credentials,
|
||||
aws_region_name=aws_region_name,
|
||||
|
|
@ -442,7 +440,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
endpoint_url=proxy_endpoint_url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
api_key=api_key,
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
## LOGGING
|
||||
|
|
|
|||
|
|
@ -202,11 +202,7 @@ async def make_call(
|
|||
|
||||
if client is None:
|
||||
_params: dict = {}
|
||||
if (
|
||||
logging_obj
|
||||
and logging_obj.litellm_params
|
||||
and logging_obj.litellm_params.get("ssl_verify")
|
||||
):
|
||||
if logging_obj and logging_obj.litellm_params and logging_obj.litellm_params.get("ssl_verify"):
|
||||
_params["ssl_verify"] = logging_obj.litellm_params.get("ssl_verify")
|
||||
if timeout is not None:
|
||||
_params["timeout"] = timeout
|
||||
|
|
@ -417,9 +413,9 @@ class BedrockLLM(BaseAWSLLM):
|
|||
|
||||
# Claude 3+ indicators (all use Messages API)
|
||||
messages_api_indicators = [
|
||||
"claude-3", # Claude 3.x models
|
||||
"claude-opus-4", # Claude Opus 4
|
||||
"claude-sonnet-4", # Claude Sonnet 4
|
||||
"claude-3", # Claude 3.x models
|
||||
"claude-opus-4", # Claude Opus 4
|
||||
"claude-sonnet-4", # Claude Sonnet 4
|
||||
"claude-haiku-4", # Claude Haiku 4
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -498,9 +498,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
value = _remove_strict_from_schema(value)
|
||||
|
||||
for tool in value:
|
||||
openai_function_object: Optional[
|
||||
ChatCompletionToolParamFunctionChunk
|
||||
] = None
|
||||
openai_function_object: Optional[ChatCompletionToolParamFunctionChunk] = (
|
||||
None
|
||||
)
|
||||
if "function" in tool: # tools list
|
||||
_openai_function_object = ChatCompletionToolParamFunctionChunk( # type: ignore
|
||||
**tool["function"]
|
||||
|
|
@ -510,19 +510,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
"parameters" in _openai_function_object
|
||||
and _openai_function_object["parameters"] is not None
|
||||
and isinstance(_openai_function_object["parameters"], dict)
|
||||
):
|
||||
if supports_response_json_schema(model):
|
||||
# Gemini 2.0+: minimal transform (resolve $ref only)
|
||||
_openai_function_object[
|
||||
"parameters"
|
||||
] = _build_vertex_schema_for_gemini_2(
|
||||
_openai_function_object["parameters"]
|
||||
)
|
||||
else:
|
||||
# Gemini 1.5: full OpenAPI-style transform
|
||||
_openai_function_object["parameters"] = _build_vertex_schema(
|
||||
_openai_function_object["parameters"]
|
||||
)
|
||||
): # OPENAI accepts JSON Schema, Google accepts OpenAPI schema.
|
||||
_openai_function_object["parameters"] = _build_vertex_schema(
|
||||
_openai_function_object["parameters"]
|
||||
)
|
||||
|
||||
openai_function_object = _openai_function_object
|
||||
|
||||
|
|
@ -641,15 +632,15 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
_tools_list.append(search_tool)
|
||||
if googleSearchRetrieval is not None:
|
||||
retrieval_tool = Tools()
|
||||
retrieval_tool[
|
||||
VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value
|
||||
] = googleSearchRetrieval
|
||||
retrieval_tool[VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value] = (
|
||||
googleSearchRetrieval
|
||||
)
|
||||
_tools_list.append(retrieval_tool)
|
||||
if enterpriseWebSearch is not None:
|
||||
enterprise_tool = Tools()
|
||||
enterprise_tool[
|
||||
VertexToolName.ENTERPRISE_WEB_SEARCH.value
|
||||
] = enterpriseWebSearch
|
||||
enterprise_tool[VertexToolName.ENTERPRISE_WEB_SEARCH.value] = (
|
||||
enterpriseWebSearch
|
||||
)
|
||||
_tools_list.append(enterprise_tool)
|
||||
if code_execution is not None:
|
||||
code_tool = Tools()
|
||||
|
|
@ -811,9 +802,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
# Check if this is gemini-3-flash which supports MINIMAL thinking level
|
||||
# Covers gemini-3-flash, gemini-3-flash-preview, gemini-3.1-flash, gemini-3.1-flash-lite-preview, etc.
|
||||
is_gemini3flash = model and (
|
||||
"gemini-3-flash" in model.lower() or "gemini-3.1-flash" in model.lower()
|
||||
"gemini-3-flash" in model.lower()
|
||||
or "gemini-3.1-flash" in model.lower()
|
||||
)
|
||||
is_gemini31pro = model and (
|
||||
"gemini-3.1-pro-preview" in model.lower()
|
||||
)
|
||||
is_gemini31pro = model and ("gemini-3.1-pro-preview" in model.lower())
|
||||
if reasoning_effort == "minimal":
|
||||
if is_gemini3flash:
|
||||
return {"thinkingLevel": "minimal", "includeThoughts": True}
|
||||
|
|
@ -1096,16 +1090,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
param_description="thinking_budget",
|
||||
)
|
||||
if VertexGeminiConfig._is_gemini_3_or_newer(model):
|
||||
optional_params[
|
||||
"thinkingConfig"
|
||||
] = VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
|
||||
effort_value, model
|
||||
optional_params["thinkingConfig"] = (
|
||||
VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
|
||||
effort_value, model
|
||||
)
|
||||
)
|
||||
else:
|
||||
optional_params[
|
||||
"thinkingConfig"
|
||||
] = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(
|
||||
effort_value, model
|
||||
optional_params["thinkingConfig"] = (
|
||||
VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(
|
||||
effort_value, model
|
||||
)
|
||||
)
|
||||
elif param == "thinking":
|
||||
# Validate no conflict with thinking_level
|
||||
|
|
@ -1114,11 +1108,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
param_name="thinking",
|
||||
param_description="thinking_budget",
|
||||
)
|
||||
optional_params[
|
||||
"thinkingConfig"
|
||||
] = VertexGeminiConfig._map_thinking_param(
|
||||
cast(AnthropicThinkingParam, value),
|
||||
model=model,
|
||||
optional_params["thinkingConfig"] = (
|
||||
VertexGeminiConfig._map_thinking_param(
|
||||
cast(AnthropicThinkingParam, value),
|
||||
model=model,
|
||||
)
|
||||
)
|
||||
elif param == "modalities" and isinstance(value, list):
|
||||
response_modalities = self.map_response_modalities(value)
|
||||
|
|
@ -1236,25 +1230,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
"IMAGE_PROHIBITED_CONTENT": "The token generation was stopped as the response was flagged for prohibited image content.",
|
||||
}
|
||||
|
||||
_GEMINI_FINISH_REASON_KEYS = frozenset(
|
||||
{
|
||||
"STOP",
|
||||
"MAX_TOKENS",
|
||||
"SAFETY",
|
||||
"RECITATION",
|
||||
"FINISH_REASON_UNSPECIFIED",
|
||||
"MALFORMED_FUNCTION_CALL",
|
||||
"LANGUAGE",
|
||||
"OTHER",
|
||||
"BLOCKLIST",
|
||||
"PROHIBITED_CONTENT",
|
||||
"SPII",
|
||||
"IMAGE_SAFETY",
|
||||
"IMAGE_PROHIBITED_CONTENT",
|
||||
"TOO_MANY_TOOL_CALLS",
|
||||
"MALFORMED_RESPONSE",
|
||||
}
|
||||
)
|
||||
_GEMINI_FINISH_REASON_KEYS = frozenset({
|
||||
"STOP", "MAX_TOKENS", "SAFETY", "RECITATION", "FINISH_REASON_UNSPECIFIED",
|
||||
"MALFORMED_FUNCTION_CALL", "LANGUAGE", "OTHER", "BLOCKLIST",
|
||||
"PROHIBITED_CONTENT", "SPII", "IMAGE_SAFETY", "IMAGE_PROHIBITED_CONTENT",
|
||||
"TOO_MANY_TOOL_CALLS", "MALFORMED_RESPONSE",
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def get_finish_reason_mapping() -> Dict[str, OpenAIChatCompletionFinishReason]:
|
||||
|
|
@ -1477,10 +1458,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
_tool_response_chunk["provider_specific_fields"] = { # type: ignore
|
||||
"thought_signature": thought_signature
|
||||
}
|
||||
_tool_response_chunk[
|
||||
"id"
|
||||
] = _encode_tool_call_id_with_signature(
|
||||
_tool_response_chunk["id"] or "", thought_signature
|
||||
_tool_response_chunk["id"] = (
|
||||
_encode_tool_call_id_with_signature(
|
||||
_tool_response_chunk["id"] or "", thought_signature
|
||||
)
|
||||
)
|
||||
_tools.append(_tool_response_chunk)
|
||||
cumulative_tool_call_idx += 1
|
||||
|
|
@ -2290,37 +2271,35 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
## ADD METADATA TO RESPONSE ##
|
||||
|
||||
setattr(model_response, "vertex_ai_grounding_metadata", grounding_metadata)
|
||||
model_response._hidden_params[
|
||||
"vertex_ai_grounding_metadata"
|
||||
] = grounding_metadata
|
||||
model_response._hidden_params["vertex_ai_grounding_metadata"] = (
|
||||
grounding_metadata
|
||||
)
|
||||
|
||||
setattr(
|
||||
model_response, "vertex_ai_url_context_metadata", url_context_metadata
|
||||
)
|
||||
|
||||
model_response._hidden_params[
|
||||
"vertex_ai_url_context_metadata"
|
||||
] = url_context_metadata
|
||||
model_response._hidden_params["vertex_ai_url_context_metadata"] = (
|
||||
url_context_metadata
|
||||
)
|
||||
|
||||
setattr(model_response, "vertex_ai_safety_results", safety_ratings)
|
||||
model_response._hidden_params[
|
||||
"vertex_ai_safety_results"
|
||||
] = safety_ratings # older approach - maintaining to prevent regressions
|
||||
model_response._hidden_params["vertex_ai_safety_results"] = (
|
||||
safety_ratings # older approach - maintaining to prevent regressions
|
||||
)
|
||||
|
||||
## ADD CITATION METADATA ##
|
||||
setattr(model_response, "vertex_ai_citation_metadata", citation_metadata)
|
||||
model_response._hidden_params[
|
||||
"vertex_ai_citation_metadata"
|
||||
] = citation_metadata # older approach - maintaining to prevent regressions
|
||||
model_response._hidden_params["vertex_ai_citation_metadata"] = (
|
||||
citation_metadata # older approach - maintaining to prevent regressions
|
||||
)
|
||||
|
||||
## ADD TRAFFIC TYPE ##
|
||||
traffic_type = completion_response.get("usageMetadata", {}).get(
|
||||
"trafficType"
|
||||
)
|
||||
if traffic_type:
|
||||
model_response._hidden_params.setdefault(
|
||||
"provider_specific_fields", {}
|
||||
)["traffic_type"] = traffic_type
|
||||
model_response._hidden_params.setdefault("provider_specific_fields", {})["traffic_type"] = traffic_type
|
||||
|
||||
except Exception as e:
|
||||
raise VertexAIError(
|
||||
|
|
@ -2407,11 +2386,7 @@ async def make_call(
|
|||
|
||||
try:
|
||||
response = await client.post(
|
||||
api_base,
|
||||
headers=headers,
|
||||
data=data,
|
||||
stream=True,
|
||||
logging_obj=logging_obj,
|
||||
api_base, headers=headers, data=data, stream=True, logging_obj=logging_obj,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
|
@ -2990,11 +2965,7 @@ class ModelResponseIterator:
|
|||
# to correctly set finish_reason="tool_calls" per the OpenAI spec.
|
||||
if not self.has_seen_tool_calls:
|
||||
for choice in model_response.choices:
|
||||
if (
|
||||
hasattr(choice, "delta")
|
||||
and choice.delta
|
||||
and choice.delta.tool_calls
|
||||
):
|
||||
if hasattr(choice, "delta") and choice.delta and choice.delta.tool_calls:
|
||||
self.has_seen_tool_calls = True
|
||||
break
|
||||
|
||||
|
|
@ -3010,10 +2981,8 @@ class ModelResponseIterator:
|
|||
if self.has_seen_tool_calls:
|
||||
mapped_finish_reason = "tool_calls"
|
||||
else:
|
||||
mapped_finish_reason = (
|
||||
VertexGeminiConfig._check_finish_reason(
|
||||
None, finish_reason_str
|
||||
)
|
||||
mapped_finish_reason = VertexGeminiConfig._check_finish_reason(
|
||||
None, finish_reason_str
|
||||
)
|
||||
choice = StreamingChoices(
|
||||
finish_reason=mapped_finish_reason,
|
||||
|
|
@ -3046,9 +3015,7 @@ class ModelResponseIterator:
|
|||
"trafficType"
|
||||
)
|
||||
if traffic_type:
|
||||
model_response._hidden_params.setdefault(
|
||||
"provider_specific_fields", {}
|
||||
)["traffic_type"] = traffic_type
|
||||
model_response._hidden_params.setdefault("provider_specific_fields", {})["traffic_type"] = traffic_type
|
||||
|
||||
setattr(model_response, "usage", usage) # type: ignore
|
||||
|
||||
|
|
|
|||
|
|
@ -249,9 +249,9 @@ def test_bedrock_invoke_async_streaming_passes_timeout_to_make_call():
|
|||
|
||||
make_call_partial = captured_partial.get("make_call")
|
||||
assert make_call_partial is not None
|
||||
assert (
|
||||
make_call_partial.keywords.get("timeout") == timeout
|
||||
), "timeout must be forwarded via partial() to make_call()"
|
||||
assert make_call_partial.keywords.get("timeout") == timeout, (
|
||||
"timeout must be forwarded via partial() to make_call()"
|
||||
)
|
||||
|
||||
|
||||
def test_bedrock_converse_async_streaming_passes_timeout_to_make_call():
|
||||
|
|
@ -311,9 +311,9 @@ def test_bedrock_converse_async_streaming_passes_timeout_to_make_call():
|
|||
asyncio.run(run())
|
||||
|
||||
_, kwargs = mock_make_call.call_args
|
||||
assert (
|
||||
kwargs.get("timeout") == timeout
|
||||
), "timeout must be forwarded to make_call() in BedrockConverseLLM.async_streaming()"
|
||||
assert kwargs.get("timeout") == timeout, (
|
||||
"timeout must be forwarded to make_call() in BedrockConverseLLM.async_streaming()"
|
||||
)
|
||||
|
||||
|
||||
def test_bedrock_converse_sync_make_sync_call_passes_timeout_to_client_post():
|
||||
|
|
@ -338,7 +338,9 @@ def test_bedrock_converse_sync_make_sync_call_passes_timeout_to_client_post():
|
|||
|
||||
timeout = httpx.Timeout(4.0)
|
||||
|
||||
with patch("litellm.llms.bedrock.chat.converse_handler.AWSEventStreamDecoder"):
|
||||
with patch(
|
||||
"litellm.llms.bedrock.chat.converse_handler.AWSEventStreamDecoder"
|
||||
):
|
||||
make_sync_call(
|
||||
client=mock_client,
|
||||
api_base="https://example.com",
|
||||
|
|
@ -351,6 +353,6 @@ def test_bedrock_converse_sync_make_sync_call_passes_timeout_to_client_post():
|
|||
)
|
||||
|
||||
_, kwargs = mock_client.post.call_args
|
||||
assert (
|
||||
kwargs.get("timeout") == timeout
|
||||
), "timeout must be forwarded to client.post() in converse make_sync_call()"
|
||||
assert kwargs.get("timeout") == timeout, (
|
||||
"timeout must be forwarded to client.post() in converse make_sync_call()"
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue