diff --git a/litellm/llms/bedrock/chat/converse_handler.py b/litellm/llms/bedrock/chat/converse_handler.py index 4ab14d8b642..3e6f7e4be7b 100644 --- a/litellm/llms/bedrock/chat/converse_handler.py +++ b/litellm/llms/bedrock/chat/converse_handler.py @@ -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 diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index e7b0760ee95..518b21ac404 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -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 ] diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 71a56e09a11..cc2fd24e906 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -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 diff --git a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py index 4ca6bf53430..bbd63c6a9eb 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py +++ b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py @@ -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()" + ) diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index cdea6981be1..94c0ddad7b4 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -237,9 +237,7 @@ def test_vertex_ai_response_json_schema_preserves_refs_for_gemini_2(): # $defs and $ref should be preserved (not unpacked) assert "response_json_schema" in transformed_request result_schema = transformed_request["response_json_schema"] - assert ( - "$defs" in result_schema - ), "responseJsonSchema should preserve $defs for Gemini 2.0+" + assert "$defs" in result_schema, "responseJsonSchema should preserve $defs for Gemini 2.0+" def test_vertex_ai_get_json_schema_preserves_refs_for_nested_pydantic(): @@ -319,22 +317,14 @@ def test_vertex_ai_response_json_schema_for_gemini_2(): # Types should be lowercase (standard JSON Schema format) assert transformed_request["response_json_schema"]["type"] == "object" - assert ( - transformed_request["response_json_schema"]["properties"]["name"]["type"] - == "string" - ) - assert ( - transformed_request["response_json_schema"]["properties"]["age"]["type"] - == "integer" - ) + assert transformed_request["response_json_schema"]["properties"]["name"]["type"] == "string" + assert transformed_request["response_json_schema"]["properties"]["age"]["type"] == "integer" # Should NOT have propertyOrdering (not needed for responseJsonSchema) assert "propertyOrdering" not in transformed_request["response_json_schema"] # additionalProperties should be preserved (supported by responseJsonSchema) - assert ( - transformed_request["response_json_schema"].get("additionalProperties") == False - ) + assert transformed_request["response_json_schema"].get("additionalProperties") == False def test_vertex_ai_response_schema_for_old_models(): @@ -591,7 +581,7 @@ def test_streaming_chunk_with_tool_calls_and_thought_includes_reasoning_content( "args": {"timezone": "America/New_York"}, }, "thoughtSignature": "EsEDCr4DAdHtim...", # Just a token, not reasoning - }, + } ] }, "finishReason": "STOP", @@ -610,18 +600,12 @@ def test_streaming_chunk_with_tool_calls_and_thought_includes_reasoning_content( streaming_chunk = iterator.chunk_parser(chunk) # Verify reasoning_content comes from the thought: true part - assert ( - streaming_chunk.choices[0].delta.reasoning_content - == "Let me think about how to get the time..." - ) + assert streaming_chunk.choices[0].delta.reasoning_content == "Let me think about how to get the time..." # Verify tool calls are also present assert streaming_chunk.choices[0].delta.tool_calls is not None assert len(streaming_chunk.choices[0].delta.tool_calls) == 1 - assert ( - streaming_chunk.choices[0].delta.tool_calls[0].function.name - == "get_current_time" - ) + assert streaming_chunk.choices[0].delta.tool_calls[0].function.name == "get_current_time" def test_streaming_chunk_with_tool_calls_no_thought_no_reasoning_content(): @@ -669,15 +653,12 @@ def test_streaming_chunk_with_tool_calls_no_thought_no_reasoning_content(): streaming_chunk = iterator.chunk_parser(chunk) # reasoning_content should be None - thoughtSignature alone does NOT mean reasoning - assert getattr(streaming_chunk.choices[0].delta, "reasoning_content", None) is None + assert getattr(streaming_chunk.choices[0].delta, 'reasoning_content', None) is None # Tool calls should still work assert streaming_chunk.choices[0].delta.tool_calls is not None assert len(streaming_chunk.choices[0].delta.tool_calls) == 1 - assert ( - streaming_chunk.choices[0].delta.tool_calls[0].function.name - == "get_current_time" - ) + assert streaming_chunk.choices[0].delta.tool_calls[0].function.name == "get_current_time" def test_check_finish_reason(): @@ -730,10 +711,7 @@ def test_vertex_ai_usage_metadata_response_token_count(): "promptTokenCount": 66, "responseTokenCount": 74, "totalTokenCount": 131, - "promptTokensDetails": [ - {"modality": "TEXT", "tokenCount": 57}, - {"modality": "IMAGE", "tokenCount": 9}, - ], + "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 57}, {"modality": "IMAGE", "tokenCount": 9}], "responseTokensDetails": [{"modality": "TEXT", "tokenCount": 74}], } usage_metadata = UsageMetadata(**usage_metadata) @@ -763,9 +741,9 @@ def test_vertex_ai_usage_metadata_with_image_tokens(): "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 14}], "candidatesTokensDetails": [ {"modality": "IMAGE", "tokenCount": 1120}, - {"modality": "TEXT", "tokenCount": 322}, # 1442 - 1120 = 322 + {"modality": "TEXT", "tokenCount": 322} # 1442 - 1120 = 322 ], - "thoughtsTokenCount": 158, + "thoughtsTokenCount": 158 } usage_metadata = UsageMetadata(**usage_metadata) result = v._calculate_usage(completion_response={"usageMetadata": usage_metadata}) @@ -807,7 +785,7 @@ def test_vertex_ai_usage_metadata_with_image_tokens_auto_calculated_text(): {"modality": "IMAGE", "tokenCount": 1120} # TEXT modality omitted - should be auto-calculated ], - "thoughtsTokenCount": 158, + "thoughtsTokenCount": 158 } usage_metadata = UsageMetadata(**usage_metadata) result = v._calculate_usage(completion_response={"usageMetadata": usage_metadata}) @@ -831,13 +809,13 @@ def test_vertex_ai_usage_metadata_with_image_tokens_auto_calculated_text(): def test_vertex_ai_usage_metadata_with_image_tokens_in_prompt(): """Test promptTokensDetails with IMAGE modality for multimodal inputs - + This test verifies the fix for issue #18182 where image_tokens were missing from prompt_tokens_details when calling Gemini models with image inputs. - + Example scenario: User sends a text prompt + image, and Gemini generates an image response. The promptTokensDetails should include both TEXT and IMAGE token counts. - + In this test case, candidatesTokenCount is INCLUSIVE of thoughtsTokenCount because: promptTokenCount (533) + candidatesTokenCount (1337) = totalTokenCount (1870) """ @@ -848,29 +826,31 @@ def test_vertex_ai_usage_metadata_with_image_tokens_in_prompt(): "totalTokenCount": 1870, "promptTokensDetails": [ {"modality": "IMAGE", "tokenCount": 527}, - {"modality": "TEXT", "tokenCount": 6}, + {"modality": "TEXT", "tokenCount": 6} ], - "candidatesTokensDetails": [{"modality": "IMAGE", "tokenCount": 1120}], - "thoughtsTokenCount": 217, + "candidatesTokensDetails": [ + {"modality": "IMAGE", "tokenCount": 1120} + ], + "thoughtsTokenCount": 217 } usage_metadata = UsageMetadata(**usage_metadata) result = v._calculate_usage(completion_response={"usageMetadata": usage_metadata}) print("result", result) - + # Verify basic token counts assert result.prompt_tokens == 533 # candidatesTokenCount is INCLUSIVE, so completion_tokens = candidatesTokenCount assert result.completion_tokens == 1337 assert result.total_tokens == 1870 - + # Verify prompt_tokens_details includes both text and image tokens assert result.prompt_tokens_details.text_tokens == 6 assert result.prompt_tokens_details.image_tokens == 527 - + # Verify completion_tokens_details assert result.completion_tokens_details.image_tokens == 1120 assert result.completion_tokens_details.reasoning_tokens == 217 - + # Verify the math: prompt_tokens = text + image # 533 = 6 (text) + 527 (image) assert ( @@ -900,17 +880,13 @@ def test_vertex_ai_map_thinking_param_with_budget_tokens_0(): def test_vertex_ai_map_tools(): v = VertexGeminiConfig() optional_params = {} - tools = v._map_function( - value=[{"code_execution": {}}], optional_params=optional_params - ) + tools = v._map_function(value=[{"code_execution": {}}], optional_params=optional_params) assert len(tools) == 1 assert tools[0]["code_execution"] == {} print(tools) new_optional_params = {} - new_tools = v._map_function( - value=[{"codeExecution": {}}], optional_params=new_optional_params - ) + new_tools = v._map_function(value=[{"codeExecution": {}}], optional_params=new_optional_params) assert len(new_tools) == 1 print("new_tools", new_tools) assert new_tools[0]["code_execution"] == {} @@ -1076,13 +1052,7 @@ def test_vertex_ai_streaming_usage_web_search_calculation(): { "content": {"parts": [{"text": "Hello"}]}, "groundingMetadata": [ - { - "webSearchQueries": [ - "", - "What is the capital of France?", - "Capital of France", - ] - } + {"webSearchQueries": ["", "What is the capital of France?", "Capital of France"]} ], } ], @@ -1426,7 +1396,7 @@ def test_vertex_ai_process_candidates_with_grounding_metadata(): def test_vertex_ai_tool_call_id_format(): """ Test that tool call IDs have the correct format and length. - + The ID should be in format 'call_' + 28 hex characters (total 33 characters). This test verifies the fix for keeping the code line under 40 characters. """ @@ -1443,7 +1413,12 @@ def test_vertex_ai_tool_call_id_format(): "args": {"location": "San Francisco", "unit": "celsius"}, } ), - HttpxPartType(functionCall={"name": "get_time", "args": {"timezone": "PST"}}), + HttpxPartType( + functionCall={ + "name": "get_time", + "args": {"timezone": "PST"} + } + ), ] function, tools, updated_idx = VertexGeminiConfig._transform_parts( @@ -1458,27 +1433,19 @@ def test_vertex_ai_tool_call_id_format(): # Test ID format for both tool calls for tool in tools: tool_id = tool["id"] - + # Should start with 'call_' - assert tool_id.startswith( - "call_" - ), f"ID should start with 'call_', got: {tool_id}" - + assert tool_id.startswith("call_"), f"ID should start with 'call_', got: {tool_id}" + # Should have exactly 33 total characters (call_ + 28 hex chars) - assert ( - len(tool_id) == 33 - ), f"ID should be 33 characters long, got {len(tool_id)}: {tool_id}" - + assert len(tool_id) == 33, f"ID should be 33 characters long, got {len(tool_id)}: {tool_id}" + # The part after 'call_' should be 28 hex characters hex_part = tool_id[5:] # Remove 'call_' prefix - assert ( - len(hex_part) == 28 - ), f"Hex part should be 28 characters, got {len(hex_part)}: {hex_part}" - + assert len(hex_part) == 28, f"Hex part should be 28 characters, got {len(hex_part)}: {hex_part}" + # Should only contain valid hex characters - assert re.match( - r"^[0-9a-f]{28}$", hex_part - ), f"Should contain only lowercase hex chars, got: {hex_part}" + assert re.match(r'^[0-9a-f]{28}$', hex_part), f"Should contain only lowercase hex chars, got: {hex_part}" # Verify IDs are unique assert tools[0]["id"] != tools[1]["id"], "Tool call IDs should be unique" @@ -1493,17 +1460,15 @@ def test_vertex_ai_tool_call_id_format(): ) if test_tools: ids_generated.add(test_tools[0]["id"]) - + # All generated IDs should be unique - assert ( - len(ids_generated) == 10 - ), f"All 10 IDs should be unique, got {len(ids_generated)} unique IDs" + assert len(ids_generated) == 10, f"All 10 IDs should be unique, got {len(ids_generated)} unique IDs" def test_vertex_ai_code_line_length(): """ Test that the specific code line generating tool call IDs is within character limit. - + This is a meta-test to ensure the code change meets the 40-character requirement. """ import inspect @@ -1513,49 +1478,45 @@ def test_vertex_ai_code_line_length(): ) # Get the source code of the _transform_parts method - source_lines = inspect.getsource(VertexGeminiConfig._transform_parts).split("\n") - + source_lines = inspect.getsource(VertexGeminiConfig._transform_parts).split('\n') + # Find the line that generates the ID id_line = None for line in source_lines: - if '"id": f"call_' in line and "uuid.uuid4().hex[:28]" in line: + if '"id": f"call_' in line and 'uuid.uuid4().hex[:28]' in line: id_line = line.strip() # Remove indentation for length check break - + assert id_line is not None, "Could not find the ID generation line in source code" - + # Check that the line is 40 characters or less (excluding indentation) line_length = len(id_line) - assert ( - line_length <= 40 - ), f"ID generation line is {line_length} characters, should be ≤40: {id_line}" - + assert line_length <= 40, f"ID generation line is {line_length} characters, should be ≤40: {id_line}" + # Verify it contains the expected UUID format - assert ( - "uuid.uuid4().hex[:28]" in id_line - ), f"Line should contain shortened UUID format: {id_line}" + assert 'uuid.uuid4().hex[:28]' in id_line, f"Line should contain shortened UUID format: {id_line}" def test_vertex_ai_map_google_maps_tool_simple(): """ Test googleMaps tool transformation without location data. - + Input: value=[{"googleMaps": {"enableWidget": "ENABLE_WIDGET"}}] optional_params={} - + Expected Output: tools=[{"googleMaps": {"enableWidget": "ENABLE_WIDGET"}}] optional_params={} (unchanged) """ v = VertexGeminiConfig() optional_params = {} - + tools = v._map_function( value=[{"googleMaps": {"enableWidget": "ENABLE_WIDGET"}}], - optional_params=optional_params, + optional_params=optional_params ) - + assert len(tools) == 1 assert "googleMaps" in tools[0] assert tools[0]["googleMaps"]["enableWidget"] == "ENABLE_WIDGET" @@ -1566,7 +1527,7 @@ def test_vertex_ai_map_google_maps_tool_with_location(): """ Test googleMaps tool transformation with location data. Verifies latitude/longitude/languageCode are extracted to toolConfig.retrievalConfig. - + Input: value=[{ "googleMaps": { @@ -1577,7 +1538,7 @@ def test_vertex_ai_map_google_maps_tool_with_location(): } }] optional_params={} - + Expected Output: tools=[{ "googleMaps": {"enableWidget": "ENABLE_WIDGET"} @@ -1596,43 +1557,40 @@ def test_vertex_ai_map_google_maps_tool_with_location(): """ v = VertexGeminiConfig() optional_params = {} - + tools = v._map_function( - value=[ - { - "googleMaps": { - "enableWidget": "ENABLE_WIDGET", - "latitude": 37.7749, - "longitude": -122.4194, - "languageCode": "en_US", - } + value=[{ + "googleMaps": { + "enableWidget": "ENABLE_WIDGET", + "latitude": 37.7749, + "longitude": -122.4194, + "languageCode": "en_US" } - ], - optional_params=optional_params, + }], + optional_params=optional_params ) - + assert len(tools) == 1 assert "googleMaps" in tools[0] - + google_maps_tool = tools[0]["googleMaps"] assert google_maps_tool["enableWidget"] == "ENABLE_WIDGET" assert "latitude" not in google_maps_tool assert "longitude" not in google_maps_tool assert "languageCode" not in google_maps_tool - + assert "toolConfig" in optional_params assert "retrievalConfig" in optional_params["toolConfig"] - + retrieval_config = optional_params["toolConfig"]["retrievalConfig"] assert retrieval_config["latLng"]["latitude"] == 37.7749 assert retrieval_config["latLng"]["longitude"] == -122.4194 assert retrieval_config["languageCode"] == "en_US" - def test_vertex_ai_penalty_parameters_validation(): """ Test that penalty parameters are properly validated for different Gemini models. - + This test ensures that: 1. Models that don't support penalty parameters (like preview models) filter them out 2. Models that support penalty parameters include them in the request @@ -1647,19 +1605,14 @@ def test_vertex_ai_penalty_parameters_validation(): for model, should_support in test_cases: # Test _supports_penalty_parameters method - assert ( - v._supports_penalty_parameters(model) == should_support - ), f"Model {model} penalty support should be {should_support}" + assert v._supports_penalty_parameters(model) == should_support, \ + f"Model {model} penalty support should be {should_support}" # Test get_supported_openai_params method supported_params = v.get_supported_openai_params(model) - has_penalty_params = ( - "frequency_penalty" in supported_params - and "presence_penalty" in supported_params - ) - assert ( - has_penalty_params == should_support - ), f"Model {model} should {'include' if should_support else 'exclude'} penalty params in supported list" + has_penalty_params = "frequency_penalty" in supported_params and "presence_penalty" in supported_params + assert has_penalty_params == should_support, \ + f"Model {model} should {'include' if should_support else 'exclude'} penalty params in supported list" # Test parameter mapping for unsupported model model = "gemini-2.5-pro-preview-06-05" @@ -1667,7 +1620,7 @@ def test_vertex_ai_penalty_parameters_validation(): "temperature": 0.7, "frequency_penalty": 0.5, "presence_penalty": 0.3, - "max_tokens": 100, + "max_tokens": 100 } optional_params = {} @@ -1675,16 +1628,12 @@ def test_vertex_ai_penalty_parameters_validation(): non_default_params=non_default_params, optional_params=optional_params, model=model, - drop_params=False, + drop_params=False ) # Penalty parameters should be filtered out for unsupported models - assert ( - "frequency_penalty" not in result - ), "frequency_penalty should be filtered out for unsupported model" - assert ( - "presence_penalty" not in result - ), "presence_penalty should be filtered out for unsupported model" + assert "frequency_penalty" not in result, "frequency_penalty should be filtered out for unsupported model" + assert "presence_penalty" not in result, "presence_penalty should be filtered out for unsupported model" # Other parameters should still be included assert "temperature" in result, "temperature should still be included" @@ -1696,7 +1645,7 @@ def test_vertex_ai_penalty_parameters_validation(): def test_vertex_ai_gemini_3_penalty_parameters_unsupported(): """ Test that penalty parameters are not supported for Gemini 3 models. - + This test ensures that: 1. Gemini 3 models do not support penalty parameters 2. Penalty parameters are excluded from supported params list for Gemini 3 models @@ -1713,25 +1662,22 @@ def test_vertex_ai_gemini_3_penalty_parameters_unsupported(): for model in gemini_3_models: # Test _supports_penalty_parameters method - assert ( - v._supports_penalty_parameters(model) == False - ), f"Gemini 3 model {model} should not support penalty parameters" + assert v._supports_penalty_parameters(model) == False, \ + f"Gemini 3 model {model} should not support penalty parameters" # Test get_supported_openai_params method supported_params = v.get_supported_openai_params(model) - assert ( - "frequency_penalty" not in supported_params - ), f"frequency_penalty should not be in supported params for {model}" - assert ( - "presence_penalty" not in supported_params - ), f"presence_penalty should not be in supported params for {model}" + assert "frequency_penalty" not in supported_params, \ + f"frequency_penalty should not be in supported params for {model}" + assert "presence_penalty" not in supported_params, \ + f"presence_penalty should not be in supported params for {model}" # Test parameter mapping - penalty params should be filtered out non_default_params = { "temperature": 0.7, "frequency_penalty": 0.5, "presence_penalty": 0.3, - "max_tokens": 100, + "max_tokens": 100 } optional_params = {} @@ -1739,46 +1685,39 @@ def test_vertex_ai_gemini_3_penalty_parameters_unsupported(): non_default_params=non_default_params, optional_params=optional_params, model=model, - drop_params=False, + drop_params=False ) # Penalty parameters should be filtered out for Gemini 3 models - assert ( - "frequency_penalty" not in result - ), f"frequency_penalty should be filtered out for Gemini 3 model {model}" - assert ( - "presence_penalty" not in result - ), f"presence_penalty should be filtered out for Gemini 3 model {model}" + assert "frequency_penalty" not in result, \ + f"frequency_penalty should be filtered out for Gemini 3 model {model}" + assert "presence_penalty" not in result, \ + f"presence_penalty should be filtered out for Gemini 3 model {model}" # Other parameters should still be included - assert ( - "temperature" in result - ), f"temperature should still be included for Gemini 3 model {model}" - assert ( - "max_output_tokens" in result - ), f"max_output_tokens should still be included for Gemini 3 model {model}" + assert "temperature" in result, \ + f"temperature should still be included for Gemini 3 model {model}" + assert "max_output_tokens" in result, \ + f"max_output_tokens should still be included for Gemini 3 model {model}" assert result["temperature"] == 0.7 assert result["max_output_tokens"] == 100 # Test that non-Gemini 3 models still support penalty parameters (if they're not in the unsupported list) non_gemini_3_model = "gemini-2.5-pro" - assert ( - v._supports_penalty_parameters(non_gemini_3_model) == True - ), f"Non-Gemini 3 model {non_gemini_3_model} should support penalty parameters" - + assert v._supports_penalty_parameters(non_gemini_3_model) == True, \ + f"Non-Gemini 3 model {non_gemini_3_model} should support penalty parameters" + supported_params = v.get_supported_openai_params(non_gemini_3_model) - assert ( - "frequency_penalty" in supported_params - ), f"frequency_penalty should be in supported params for {non_gemini_3_model}" - assert ( - "presence_penalty" in supported_params - ), f"presence_penalty should be in supported params for {non_gemini_3_model}" + assert "frequency_penalty" in supported_params, \ + f"frequency_penalty should be in supported params for {non_gemini_3_model}" + assert "presence_penalty" in supported_params, \ + f"presence_penalty should be in supported params for {non_gemini_3_model}" def test_vertex_ai_annotation_streaming_events(): """ Test that annotation events are properly emitted during streaming for Vertex AI Gemini. - + This test verifies: 1. Grounding metadata is converted to annotations in streaming chunks 2. Annotations are included in the delta of streaming chunks @@ -1801,7 +1740,7 @@ def test_vertex_ai_annotation_streaming_events(): "groundingMetadata": { "webSearchQueries": ["weather San Francisco today"], "searchEntryPoint": { - "renderedContent": "