Merge branch 'main' into litellm_compliance_checker_ui

This commit is contained in:
Ishaan Jaffer 2026-02-17 16:36:19 -08:00
commit 11252bb83f
6 changed files with 645 additions and 101 deletions

View file

@ -329,10 +329,10 @@ class ContentFilterGuardrail(CustomGuardrail):
action if action else category_config_obj.default_action
)
# Handle conditional categories (with identifier_words + inherit_from)
if (
category_config_obj.identifier_words
and category_config_obj.inherit_from
# Handle conditional categories (with identifier_words + block words)
if category_config_obj.identifier_words and (
category_config_obj.inherit_from
or category_config_obj.additional_block_words
):
self._load_conditional_category(
category_name,
@ -387,51 +387,55 @@ class ContentFilterGuardrail(CustomGuardrail):
categories_dir: str,
) -> None:
"""
Load a conditional category that uses identifier_words + inherited block_words.
Load a conditional category that uses identifier_words + block_words.
Block words can come from inherited category or additional_block_words.
Args:
category_name: Name of the category
category_config_obj: CategoryConfig object with identifier_words and inherit_from
category_config_obj: CategoryConfig object with identifier_words
category_action: Action to take when match is found
severity_threshold: Minimum severity threshold
categories_dir: Directory containing category files
"""
# Load the inherited category to get block words
inherit_from = category_config_obj.inherit_from
if not inherit_from:
return
# Remove .json or .yaml extension if included
inherit_base = inherit_from.replace(".json", "").replace(".yaml", "")
# Find the inherited category file
inherit_yaml_path = os.path.join(categories_dir, f"{inherit_base}.yaml")
inherit_json_path = os.path.join(categories_dir, f"{inherit_base}.json")
if os.path.exists(inherit_yaml_path):
inherit_file_path = inherit_yaml_path
elif os.path.exists(inherit_json_path):
inherit_file_path = inherit_json_path
else:
verbose_proxy_logger.warning(
f"Category {category_name}: inherit_from '{inherit_from}' file not found at {categories_dir}"
)
verbose_proxy_logger.debug(
f"Tried paths: {inherit_yaml_path}, {inherit_json_path}"
)
return
try:
# Load the inherited category
inherited_category = self._load_category_file(inherit_file_path)
# Extract block words from inherited category that meet severity threshold
block_words = []
for keyword_data in inherited_category.keywords:
keyword = keyword_data["keyword"].lower()
severity = keyword_data["severity"]
if self._should_apply_severity(severity, severity_threshold):
block_words.append(keyword)
inherit_from = category_config_obj.inherit_from
# Load inherited block words if specified
if inherit_from:
# Remove .json or .yaml extension if included
inherit_base = inherit_from.replace(".json", "").replace(".yaml", "")
# Find the inherited category file
inherit_yaml_path = os.path.join(categories_dir, f"{inherit_base}.yaml")
inherit_json_path = os.path.join(categories_dir, f"{inherit_base}.json")
inherit_file_path = None
if os.path.exists(inherit_yaml_path):
inherit_file_path = inherit_yaml_path
elif os.path.exists(inherit_json_path):
inherit_file_path = inherit_json_path
else:
verbose_proxy_logger.warning(
f"Category {category_name}: inherit_from '{inherit_from}' file not found at {categories_dir}"
)
verbose_proxy_logger.debug(
f"Tried paths: {inherit_yaml_path}, {inherit_json_path}"
)
if inherit_file_path:
# Load the inherited category
inherited_category = self._load_category_file(inherit_file_path)
# Extract block words from inherited category that meet severity threshold
for keyword_data in inherited_category.keywords:
keyword = keyword_data["keyword"].lower()
severity = keyword_data["severity"]
if self._should_apply_severity(severity, severity_threshold):
block_words.append(keyword)
else:
# If inherit file not found, set inherit_from to None for logging
inherit_from = None
# Add additional block words specific to this category
if category_config_obj.additional_block_words:
@ -445,16 +449,29 @@ class ContentFilterGuardrail(CustomGuardrail):
"severity": "high", # Combinations are always high severity
}
verbose_proxy_logger.info(
# Build log message
log_msg = (
f"Loaded conditional category {category_name}: "
f"{len(category_config_obj.identifier_words)} identifiers + "
f"{len(block_words)} block words "
f"({len(category_config_obj.additional_block_words)} additional + "
f"{len(block_words) - len(category_config_obj.additional_block_words)} from {inherit_from})"
f"{len(block_words)} block words"
)
if inherit_from and category_config_obj.additional_block_words:
inherited_count = len(block_words) - len(
category_config_obj.additional_block_words
)
log_msg += (
f" ({len(category_config_obj.additional_block_words)} additional + "
f"{inherited_count} from {inherit_from})"
)
elif inherit_from:
log_msg += f" (from {inherit_from})"
elif category_config_obj.additional_block_words:
log_msg += f" ({len(block_words)} from additional_block_words)"
verbose_proxy_logger.info(log_msg)
except Exception as e:
verbose_proxy_logger.error(
f"Error loading inherited category for {category_name}: {e}"
f"Error loading conditional category for {category_name}: {e}"
)
def _load_category_file(self, file_path: str) -> CategoryConfig:

View file

@ -0,0 +1,242 @@
# Loi sur l'IA (UE) Article 5 - Détection des pratiques interdites
# Utilise une logique conditionnelle : BLOQUER si un mot identificateur + un mot de blocage apparaissent ensemble
# Référence : https://artificialintelligenceact.eu/article/5/
category_name: "eu_ai_act_article5_prohibited_practices_fr"
description: "Détecte les pratiques interdites de l'Article 5 de la loi sur l'IA de l'UE (français)"
default_action: "BLOCK"
# MOTS IDENTIFICATEURS - Actions qui pourraient créer des systèmes interdits
identifier_words:
# Actions de développement
- "construire"
- "créer"
- "développer"
- "concevoir"
- "implémenter"
- "mettre en place"
- "établir"
- "bâtir"
- "élaborer"
# Actions de notation/évaluation
- "noter"
- "classer"
- "évaluer"
- "estimer"
- "classifier"
- "profiler"
- "coter"
- "juger"
- "attribuer une note"
- "attribuer un score"
- "donner une note"
- "donner un score"
# Actions de détection
- "reconnaître"
- "détecter"
- "identifier"
- "inférer"
- "déduire"
- "prédire"
- "déterminer"
- "analyser"
- "repérer"
- "déceler"
- "mesurer"
- "surveiller"
- "monitorer"
# MOTS DE BLOCAGE SUPPLÉMENTAIRES - Contextes interdits
additional_block_words:
# Notation sociale (Article 5.1.c)
- "comportement social"
- "crédit social"
- "score social"
- "note sociale"
- "notation sociale"
- "fiabilité"
- "score de citoyen"
- "note de citoyen"
- "score de réputation"
- "note de réputation"
- "score comportemental"
- "note comportementale"
- "comportement sur les réseaux sociaux"
- "comportement médias sociaux"
- "réputation sociale"
# Reconnaissance d'émotions au travail/éducation (Article 5.1.f)
- "émotion des employés"
- "émotion des salariés"
- "émotion du personnel"
- "émotion au travail"
- "émotion des travailleurs"
- "émotion des étudiants"
- "émotion des élèves"
- "émotion en classe"
- "sentiment des employés"
- "sentiment des salariés"
- "sentiment des travailleurs"
- "sentiment des étudiants"
- "sentiment des élèves"
- "humeur des employés"
- "humeur des salariés"
- "humeur des travailleurs"
- "état émotionnel employés"
- "état émotionnel salariés"
- "ressenti des employés"
- "ressenti des salariés"
# Catégorisation biométrique (Article 5.1.h)
- "race à partir du visage"
- "ethnie à partir du visage"
- "race à partir de biométrie"
- "race à partir du biométrique"
- "ethnie à partir de biométrie"
- "ethnie à partir du biométrique"
- "opinions politiques à partir"
- "vues politiques à partir"
- "orientation sexuelle à partir"
- "religion à partir de biométrie"
- "religion à partir du biométrique"
- "croyances religieuses à partir"
# Profilage prédictif
- "prédiction de crime"
- "prédiction criminelle"
- "comportement criminel"
- "récidive"
- "prédiction de la récidive"
- "prédiction de performance des employés"
- "prédiction de performance des salariés"
- "solvabilité à partir des réseaux sociaux"
- "solvabilité à partir du social"
# Manipulation (Article 5.1.a)
- "subliminal"
- "subliminale"
- "subconscient"
- "inconscient"
- "manipulation de comportement"
- "manipulation comportementale"
# Exploitation de vulnérabilités (Article 5.1.b)
- "vulnérabilité des enfants"
- "vulnérabilité des personnes âgées"
- "vulnérabilité des handicapés"
- "vulnérabilité des personnes handicapées"
# TOUJOURS BLOQUER - Pratiques interdites explicites
always_block_keywords:
# Systèmes de notation sociale
- keyword: "système de crédit social"
severity: "high"
- keyword: "système de notation sociale"
severity: "high"
- keyword: "système de score social"
severity: "high"
- keyword: "notation des citoyens"
severity: "high"
- keyword: "système de notation des citoyens"
severity: "high"
- keyword: "système pour noter le comportement social"
severity: "high"
- keyword: "système pour évaluer le comportement social"
severity: "high"
- keyword: "système pour classer le comportement social"
severity: "high"
# Reconnaissance d'émotions au travail/éducation
- keyword: "reconnaissance d'émotions au travail"
severity: "high"
- keyword: "détection d'émotions des employés"
severity: "high"
- keyword: "détection d'émotions des salariés"
severity: "high"
- keyword: "reconnaissance d'émotions en classe"
severity: "high"
- keyword: "détection d'émotions des étudiants"
severity: "high"
- keyword: "détection d'émotions des élèves"
severity: "high"
# Catégorisation biométrique
- keyword: "inférer la race à partir du visage"
severity: "high"
- keyword: "prédire la race à partir du visage"
severity: "high"
- keyword: "déduire la race à partir du visage"
severity: "high"
- keyword: "inférer l'ethnie à partir de biométrie"
severity: "high"
- keyword: "prédire l'ethnie à partir de biométrie"
severity: "high"
- keyword: "prédire les opinions politiques à partir"
severity: "high"
- keyword: "inférer les opinions politiques à partir"
severity: "high"
- keyword: "système de catégorisation biométrique"
severity: "high"
# Profilage prédictif
- keyword: "police prédictive"
severity: "high"
- keyword: "algorithme de prédiction de crime"
severity: "high"
- keyword: "algorithme de prédiction criminelle"
severity: "high"
- keyword: "prédiction de récidive"
severity: "high"
# EXCEPTIONS - Cas d'utilisation légitimes
exceptions:
# Recherche et éducation
- "recherche sur"
- "étude sur"
- "académique"
- "thèse sur"
- "mémoire sur"
- "recherche scientifique"
- "travail de recherche"
# Surveillance de conformité
- "audit de biais"
- "audit des biais"
- "détecter la discrimination"
- "détection de discrimination"
- "surveillance de conformité"
- "contrôle de conformité"
- "examen éthique"
- "révision éthique"
- "test d'équité"
- "test de justice"
# Contextes de divertissement/produits
- "dans un film"
- "dans le film"
- "pour un film"
- "cinéma"
- "dans un jeu"
- "dans le jeu"
- "jeu vidéo"
- "avis sur le produit"
- "avis produit"
- "retour client"
- "commentaires clients"
- "feedback client"
# Méta-discussion
- "expliquer"
- "qu'est-ce que"
- "c'est quoi"
- "article 5"
- "interdit par"
- "prohibé par"
- "en quoi consiste"
- "de manière équitable"
- "de façon équitable"
- "équitablement"
- "de manière juste"
- "de façon juste"
- "justement évaluer"

View file

@ -0,0 +1,292 @@
"""
Test EU AI Act Article 5 - French Language Support (3 Critical Scenarios)
Tests the exact 3 scenarios requested:
1. Request 1: French query that should be blocked (fails/blocked)
2. Request 2: User tries to circumvent the check (also fails/blocked)
3. Request 3: Safe query in French that should pass (allowed)
"""
import sys
import os
import pytest
sys.path.insert(0, os.path.abspath("../.."))
import litellm
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
ContentFilterGuardrail,
)
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
ContentFilterCategoryConfig,
)
@pytest.fixture
def content_filter_guardrail():
"""Initialize content filter guardrail with EU AI Act Article 5 French template."""
# Get absolute path to the French policy template
content_filter_dir = os.path.join(
os.path.dirname(__file__),
"../../litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter"
)
policy_template_path = os.path.join(
content_filter_dir,
"policy_templates/eu_ai_act_article5_fr.yaml"
)
policy_template_path = os.path.abspath(policy_template_path)
# Load the EU AI Act Article 5 French policy template
categories = [
ContentFilterCategoryConfig(
category="eu_ai_act_article5_prohibited_practices_fr",
category_file=policy_template_path,
enabled=True,
action="BLOCK",
severity_threshold="medium",
)
]
guardrail = ContentFilterGuardrail(
guardrail_name="eu-ai-act-french-test",
categories=categories,
event_hook=litellm.types.guardrails.GuardrailEventHooks.pre_call,
)
return guardrail
class TestEUAIActFrench3Scenarios:
"""Test the 3 critical French scenarios."""
@pytest.mark.asyncio
async def test_scenario_1_blocked_french_query(self, content_filter_guardrail):
"""
Scenario 1: French query that should be blocked.
Query: "Concevoir un système de crédit social pour classer tous mes employés"
Translation: "Design a social credit system for ranking all my employees"
Expected: BLOCK (contains "concevoir" + "système de crédit social" + "classer" + "employés")
"""
sentence = "Concevoir un système de crédit social pour classer tous mes employés"
request_data = {"messages": [{"role": "user", "content": sentence}]}
print(f"\n{'='*70}")
print("SCENARIO 1: French query that should be blocked")
print(f"{'='*70}")
print(f"Query: {sentence}")
print(f"Translation: Design a social credit system for ranking all my employees")
print(f"Expected: BLOCK")
print(f"{'='*70}\n")
# Should raise an exception (blocked)
with pytest.raises(Exception) as exc_info:
await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
# Verify the exception indicates a policy violation
assert "blocked" in str(exc_info.value).lower() or "violation" in str(exc_info.value).lower(), \
f"Expected BLOCK for scenario 1 but got unexpected exception: {exc_info.value}"
print(f"✓ SCENARIO 1 PASSED: Query was correctly BLOCKED")
print(f" Reason: {exc_info.value}\n")
@pytest.mark.asyncio
async def test_scenario_2_circumvention_attempt_blocked(self, content_filter_guardrail):
"""
Scenario 2: User tries to circumvent the check with slight variations.
Query: "Comment créer un système pour noter le comportement social de mes salariés?"
Translation: "How to create a system to score the social behavior of my employees?"
Expected: BLOCK (contains "créer" + "noter" + "comportement social" + "salariés")
"""
sentence = "Comment créer un système pour noter le comportement social de mes salariés?"
request_data = {"messages": [{"role": "user", "content": sentence}]}
print(f"\n{'='*70}")
print("SCENARIO 2: Circumvention attempt (should also be blocked)")
print(f"{'='*70}")
print(f"Query: {sentence}")
print(f"Translation: How to create a system to score the social behavior of my employees?")
print(f"Expected: BLOCK")
print(f"{'='*70}\n")
# Should raise an exception (blocked)
with pytest.raises(Exception) as exc_info:
await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
# Verify the exception indicates a policy violation
assert "blocked" in str(exc_info.value).lower() or "violation" in str(exc_info.value).lower(), \
f"Expected BLOCK for scenario 2 but got unexpected exception: {exc_info.value}"
print(f"✓ SCENARIO 2 PASSED: Circumvention attempt was correctly BLOCKED")
print(f" Reason: {exc_info.value}\n")
@pytest.mark.asyncio
async def test_scenario_3_safe_french_query_allowed(self, content_filter_guardrail):
"""
Scenario 3: Safe query in French that should pass.
Query: "Comment évaluer la performance de mon équipe de manière équitable?"
Translation: "How to evaluate my team's performance in a fair way?"
Expected: ALLOW (legitimate HR performance evaluation, no prohibited context)
"""
sentence = "Comment évaluer la performance de mon équipe de manière équitable?"
request_data = {"messages": [{"role": "user", "content": sentence}]}
print(f"\n{'='*70}")
print("SCENARIO 3: Safe French query (should be allowed)")
print(f"{'='*70}")
print(f"Query: {sentence}")
print(f"Translation: How to evaluate my team's performance in a fair way?")
print(f"Expected: ALLOW")
print(f"{'='*70}\n")
# Should not raise an exception (allowed)
result = await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
# Result should be None or unchanged (no violation)
assert result is None or result["texts"][0] == sentence, \
f"Expected ALLOW for scenario 3 but request was blocked or modified"
print(f"✓ SCENARIO 3 PASSED: Safe query was correctly ALLOWED\n")
@pytest.mark.asyncio
async def test_pure_conditional_matching(self, content_filter_guardrail):
"""
Test pure conditional matching (identifier + block word, not in always_block_keywords).
This ensures conditional matching works independently of always_block_keywords.
"""
# This sentence contains "analyser" (identifier) + "sentiment des salariés" (block word)
# but doesn't match any always_block_keywords
sentence = "Développer un outil pour analyser le sentiment des salariés en continu"
request_data = {"messages": [{"role": "user", "content": sentence}]}
print(f"\n{'='*70}")
print("PURE CONDITIONAL MATCHING TEST")
print(f"{'='*70}")
print(f"Query: {sentence}")
print(f"Translation: Develop a tool to analyze employee sentiment continuously")
print(f"Expected: BLOCK (conditional match: analyser + sentiment des salariés)")
print(f"{'='*70}\n")
# Should raise an exception (blocked by conditional matching)
with pytest.raises(Exception) as exc_info:
await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
# Verify it's a conditional match, not an always_block match
error_detail = getattr(exc_info.value, 'detail', {})
error_msg = error_detail.get("error", str(exc_info.value)) if isinstance(error_detail, dict) else str(exc_info.value)
assert "conditional match" in error_msg.lower(), \
f"Expected conditional match but got: {error_detail}"
print(f"✓ PURE CONDITIONAL MATCHING PASSED")
print(f" Reason: {exc_info.value}\n")
# Additional edge cases for French language support
class TestFrenchEdgeCases:
"""Test additional French language edge cases."""
@pytest.mark.asyncio
async def test_mixed_french_english(self, content_filter_guardrail):
"""Test mixed French and English query."""
sentence = "Build a système de crédit social for employees"
request_data = {"messages": [{"role": "user", "content": sentence}]}
# Should block (contains "build" and "système de crédit social")
with pytest.raises(Exception):
await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
@pytest.mark.asyncio
async def test_french_research_exception(self, content_filter_guardrail):
"""Test French research exception."""
sentence = "Je fais une recherche sur les systèmes de crédit social en Chine"
request_data = {"messages": [{"role": "user", "content": sentence}]}
# Should allow (contains "recherche sur" exception)
result = await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
assert result is None or result["texts"][0] == sentence
@pytest.mark.asyncio
async def test_french_case_insensitive(self, content_filter_guardrail):
"""Test case-insensitive matching in French."""
sentence = "CONCEVOIR UN SYSTÈME DE CRÉDIT SOCIAL"
request_data = {"messages": [{"role": "user", "content": sentence}]}
# Should block (case-insensitive)
with pytest.raises(Exception):
await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
@pytest.mark.asyncio
async def test_exception_bypass_prevention(self, content_filter_guardrail):
"""
Test that short exception words don't create bypasses.
Words like "enjeu" (stake) should not match "jeu" (game) exception.
"""
# "enjeu" contains "jeu" but should NOT trigger exception
sentence = "Créer un système de crédit social pour l'enjeu principal de l'entreprise"
request_data = {"messages": [{"role": "user", "content": sentence}]}
# Should still block (no exception bypass)
with pytest.raises(Exception) as exc_info:
await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
# Verify it was blocked
assert "blocked" in str(exc_info.value).lower()
@pytest.mark.asyncio
async def test_legitimate_game_context_allowed(self, content_filter_guardrail):
"""Test that legitimate game context with proper phrasing is allowed."""
sentence = "Détecter les émotions des joueurs dans un jeu vidéo"
request_data = {"messages": [{"role": "user", "content": sentence}]}
# Should allow (contains "dans un jeu" exception with proper context)
result = await content_filter_guardrail.apply_guardrail(
inputs={"texts": [sentence]},
request_data=request_data,
input_type="request",
)
assert result is None or result["texts"][0] == sentence
if __name__ == "__main__":
# Run tests with: pytest test_eu_ai_act_french_3_scenarios.py -v -s
pytest.main([__file__, "-v", "-s"])

View file

@ -49,6 +49,12 @@ def isolate_litellm_state():
if hasattr(litellm, '_async_failure_callback'):
original_state['_async_failure_callback'] = litellm._async_failure_callback.copy() if litellm._async_failure_callback else []
# Store transport/network globals — many tests set these without restoring,
# causing subsequent tests to get None from _create_async_transport()
for _attr in ('disable_aiohttp_transport', 'force_ipv4'):
if hasattr(litellm, _attr):
original_state[_attr] = getattr(litellm, _attr)
# Flush cache before test (critical for respx mocks)
if hasattr(litellm, "in_memory_llm_clients_cache"):
litellm.in_memory_llm_clients_cache.flush_cache()

View file

@ -582,14 +582,15 @@ class TestTokenizerSelection(unittest.TestCase):
@patch("litellm.utils._return_huggingface_tokenizer")
def test_disable_hf_tokenizer_download(self, mock_return_huggingface_tokenizer):
# Use pytest.MonkeyPatch() directly instead of fixture
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(litellm, "disable_hf_tokenizer_download", True)
result = _select_tokenizer_helper("grok-32r22r")
mock_return_huggingface_tokenizer.assert_not_called()
assert result["type"] == "openai_tokenizer"
assert result["tokenizer"] == encoding
try:
result = _select_tokenizer_helper("grok-32r22r")
mock_return_huggingface_tokenizer.assert_not_called()
assert result["type"] == "openai_tokenizer"
assert result["tokenizer"] == encoding
finally:
monkeypatch.undo()
@pytest.mark.parametrize(

View file

@ -1,9 +1,8 @@
import json
import os
import sys
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
sys.path.insert(
@ -16,6 +15,14 @@ from litellm.llms.vertex_ai.vertex_ai_partner_models.gpt_oss.transformation impo
)
@pytest.fixture(autouse=True)
def _reset_litellm_http_client_cache():
"""Ensure each test gets a fresh async HTTP client mock."""
from litellm import in_memory_llm_clients_cache
in_memory_llm_clients_cache.flush_cache()
@pytest.fixture(autouse=True)
def clean_vertex_env():
"""Clear Google/Vertex AI environment variables before each test to prevent test isolation issues."""
@ -76,11 +83,6 @@ async def test_vertex_ai_gpt_oss_simple_request():
Test that a simple request to vertex_ai/openai/gpt-oss-20b-maas lands at the correct URL
with the correct request body.
"""
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexLLM,
)
# Mock response
mock_response = MagicMock()
mock_response.status_code = 200
@ -107,21 +109,17 @@ async def test_vertex_ai_gpt_oss_simple_request():
}
}
client = AsyncHTTPHandler()
async def mock_post_func(*args, **kwargs):
return mock_response
# Mock vertexai module to prevent import from triggering authentication
mock_vertexai = MagicMock()
mock_vertexai.preview = MagicMock()
mock_vertexai.preview.language_models = MagicMock()
with patch.object(client, "post", side_effect=mock_post_func) as mock_post, \
patch.object(VertexLLM, "_ensure_access_token", return_value=("fake-token", "pathrise-convert-1606954137718")), \
patch('litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini.VertexAIError', Exception), \
patch.dict('sys.modules', {'vertexai': mock_vertexai, 'vertexai.preview': mock_vertexai.preview}), \
with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler") as mock_http_handler, \
patch("litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini.VertexLLM._ensure_access_token",
return_value=("fake-token", "pathrise-convert-1606954137718")), \
patch.dict("sys.modules", {"vertexai": mock_vertexai, "vertexai.preview": mock_vertexai.preview}), \
patch.dict(os.environ, {"VERTEXAI_PROJECT": "pathrise-convert-1606954137718"}):
mock_http_handler.return_value.post = AsyncMock(return_value=mock_response)
response = await litellm.acompletion(
model="vertex_ai/openai/gpt-oss-20b-maas",
messages=[
@ -136,22 +134,20 @@ async def test_vertex_ai_gpt_oss_simple_request():
],
vertex_ai_location="us-central1",
vertex_ai_project="pathrise-convert-1606954137718",
client=client
)
# Verify the mock was called
mock_post.assert_called_once()
mock_http_handler.return_value.post.assert_called_once()
# Get the call arguments
call_args = mock_post.call_args
# For side_effect, the URL is passed as kwargs['url']
call_args = mock_http_handler.return_value.post.call_args
called_url = call_args.kwargs["url"]
request_body = json.loads(call_args.kwargs["data"])
# Verify the URL
expected_url = "https://us-central1-aiplatform.googleapis.com/v1/projects/pathrise-convert-1606954137718/locations/us-central1/endpoints/openapi/chat/completions"
assert called_url == expected_url
# Verify the request body
expected_request_body = {
'model': 'openai/gpt-oss-20b-maas',
@ -168,7 +164,7 @@ async def test_vertex_ai_gpt_oss_simple_request():
'stream': False
}
assert request_body == expected_request_body
# Verify response structure
assert response.model == "openai/gpt-oss-20b-maas"
assert len(response.choices) == 1
@ -181,11 +177,6 @@ async def test_vertex_ai_gpt_oss_reasoning_effort():
Test that reasoning_effort parameter is correctly passed in the request body
for GPT-OSS models.
"""
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexLLM,
)
# Mock response
mock_response = MagicMock()
mock_response.status_code = 200
@ -212,21 +203,17 @@ async def test_vertex_ai_gpt_oss_reasoning_effort():
}
}
client = AsyncHTTPHandler()
async def mock_post_func(*args, **kwargs):
return mock_response
# Mock vertexai module to prevent import from triggering authentication
mock_vertexai = MagicMock()
mock_vertexai.preview = MagicMock()
mock_vertexai.preview.language_models = MagicMock()
with patch.object(client, "post", side_effect=mock_post_func) as mock_post, \
patch.object(VertexLLM, "_ensure_access_token", return_value=("fake-token", "pathrise-convert-1606954137718")), \
patch('litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini.VertexAIError', Exception), \
patch.dict('sys.modules', {'vertexai': mock_vertexai, 'vertexai.preview': mock_vertexai.preview}), \
with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler") as mock_http_handler, \
patch("litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini.VertexLLM._ensure_access_token",
return_value=("fake-token", "pathrise-convert-1606954137718")), \
patch.dict("sys.modules", {"vertexai": mock_vertexai, "vertexai.preview": mock_vertexai.preview}), \
patch.dict(os.environ, {"VERTEXAI_PROJECT": "pathrise-convert-1606954137718"}):
mock_http_handler.return_value.post = AsyncMock(return_value=mock_response)
response = await litellm.acompletion(
model="vertex_ai/openai/gpt-oss-20b-maas",
messages=[
@ -242,20 +229,19 @@ async def test_vertex_ai_gpt_oss_reasoning_effort():
reasoning_effort="low",
vertex_ai_location="us-central1",
vertex_ai_project="pathrise-convert-1606954137718",
client=client
)
# Verify the mock was called
mock_post.assert_called_once()
# Get the call arguments
call_args = mock_post.call_args
mock_http_handler.return_value.post.assert_called_once()
# Get the call arguments
call_args = mock_http_handler.return_value.post.call_args
request_body = json.loads(call_args.kwargs["data"])
# Verify reasoning_effort is in the request body
assert "reasoning_effort" in request_body
assert request_body["reasoning_effort"] == "low"
# Verify other expected fields
expected_request_body = {
'model': 'openai/gpt-oss-20b-maas',
@ -273,7 +259,7 @@ async def test_vertex_ai_gpt_oss_reasoning_effort():
'stream': False
}
assert request_body == expected_request_body
# Verify response structure
assert response.model == "openai/gpt-oss-20b-maas"
assert len(response.choices) == 1