mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
test: require a match= on broad pytest.raises, and drop duplicate parametrize cases (#37769)
`pytest.raises(Exception)` with no `match=` passes on any error that broad. A TypeError from a refactor, a botched fixture, an import that moved: all of them read as the rejection the test claims to police, so the test goes green for the wrong reason and stays green after the behaviour it guards is gone. PT011 closes that gap for the 317 sites B017 could not reach, because B017 only fires on a single-statement body with no `as e` binding. Each pattern here is the message the code actually raised, recorded by running the sites under a plugin that logged the concrete type and text per call site, so the assertions describe observed behaviour rather than a guess. Where a site raises more than one message across its parametrize cases, the pattern is an alternation of what was seen; where the exception carries an empty `str()` and puts the text on `.message`, the site keeps a narrow `noqa` with the reason. PT014 removes four parametrize cases that were listed twice. The duplicate re-runs an assertion that already passed, and it usually marks a case someone meant to vary and forgot to edit.
This commit is contained in:
parent
354f497faf
commit
b76def0e5d
145 changed files with 844 additions and 867 deletions
|
|
@ -20,6 +20,12 @@
|
|||
# PT012 a `pytest.raises` block that runs on past the raising call. Everything after
|
||||
# that call is dead, so an `assert` sitting there is never checked. Keep the
|
||||
# block to the call itself and put the assertions below it
|
||||
# PT011 `pytest.raises(Exception)` / `(ValueError)` / `(OSError)` with no `match=`. The
|
||||
# block passes on any error that broad, so the TypeError a refactor introduced
|
||||
# reads as the rejection under test. Pin the message the code actually raises
|
||||
# PT014 the same `parametrize` case listed twice. The copy re-runs an assertion that
|
||||
# already passed and adds no coverage, and it usually marks a case someone meant
|
||||
# to vary and forgot to edit
|
||||
#
|
||||
# No target-version here on purpose: it resolves from requires-python (>=3.10), so
|
||||
# 3.11-only builtins like BaseExceptionGroup are correctly flagged in a tree that
|
||||
|
|
@ -27,4 +33,4 @@
|
|||
|
||||
line-length = 120
|
||||
|
||||
lint.select = ["F821", "B011", "B015", "B017", "B018", "PT012", "PT015", "PLR0133", "PLW0127"]
|
||||
lint.select = ["F821", "B011", "B015", "B017", "B018", "PT011", "PT012", "PT014", "PT015", "PLR0133", "PLW0127"]
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ def test_invalid_metric_name_validation():
|
|||
litellm.prometheus_metrics_config = test_config
|
||||
|
||||
# Creating PrometheusLogger should raise ValueError due to invalid metric
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Configuration validation failed') as exc_info:
|
||||
PrometheusLogger()
|
||||
|
||||
# Verify error message contains information about invalid metric
|
||||
|
|
@ -429,7 +429,7 @@ def test_invalid_labels_validation():
|
|||
litellm.prometheus_metrics_config = test_config
|
||||
|
||||
# Creating PrometheusLogger should raise ValueError due to invalid labels
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Configuration validation failed') as exc_info:
|
||||
PrometheusLogger()
|
||||
|
||||
# Verify error message contains information about invalid labels
|
||||
|
|
@ -598,7 +598,7 @@ def test_invalid_exclude_metric_name_raises(reset_prometheus_exclude_settings):
|
|||
litellm.prometheus_exclude_labels = None
|
||||
litellm.prometheus_exclude_metrics = ["not_a_real_metric"]
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Prometheus exclude configuration validation failed') as exc_info:
|
||||
PrometheusLogger()
|
||||
|
||||
assert "not_a_real_metric" in str(exc_info.value)
|
||||
|
|
@ -612,7 +612,7 @@ def test_invalid_exclude_label_name_raises(reset_prometheus_exclude_settings):
|
|||
litellm.prometheus_exclude_metrics = None
|
||||
litellm.prometheus_exclude_labels = ["not_a_real_label"]
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Prometheus exclude configuration validation failed') as exc_info:
|
||||
PrometheusLogger()
|
||||
|
||||
assert "not_a_real_label" in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ async def test_bedrock_apply_guardrail_api_failure():
|
|||
mock_api_request.side_effect = Exception("API connection failed")
|
||||
|
||||
# Test the apply_guardrail method should raise an exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Bedrock guardrail failed: API connection failed') as exc_info:
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["This is a test message"]},
|
||||
request_data={},
|
||||
|
|
|
|||
|
|
@ -1653,7 +1653,7 @@ async def test_afile_retrieve_raises_error_when_no_router_and_file_object_none()
|
|||
|
||||
unified_file_id = "test-unified-file-id"
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='LiteLLM Managed File object with id=test-unified-file-id') as exc_info:
|
||||
await proxy_managed_files.afile_retrieve(
|
||||
file_id=unified_file_id,
|
||||
litellm_parent_otel_span=None,
|
||||
|
|
@ -1719,7 +1719,7 @@ async def test_afile_retrieve_raises_error_for_non_managed_file():
|
|||
# Mock get_unified_file_id to return None (file not found)
|
||||
proxy_managed_files.get_unified_file_id = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='LiteLLM Managed File object with id=non-existent-file-id') as exc_info:
|
||||
await proxy_managed_files.afile_retrieve(
|
||||
file_id="non-existent-file-id",
|
||||
litellm_parent_otel_span=None,
|
||||
|
|
@ -2027,7 +2027,7 @@ async def test_list_batches_from_managed_objects_table_provider_filter_raises_ex
|
|||
)
|
||||
|
||||
# Filtering by provider should raise Exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Filtering by 'provider' is not supported when using managed") as exc_info:
|
||||
await proxy_managed_files.list_user_batches(
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="test-user"),
|
||||
limit=10,
|
||||
|
|
@ -2053,7 +2053,7 @@ async def test_list_batches_from_managed_objects_table_target_model_name_filter_
|
|||
)
|
||||
|
||||
# Filtering by provider should raise Exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Filtering by 'target_model_names' is not supported when") as exc_info:
|
||||
await proxy_managed_files.list_user_batches(
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="test-user"),
|
||||
limit=10,
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ def test_check_team_project_limits_models_not_in_team():
|
|||
models=["gpt-5.5", "claude-3"], # claude-3 not in team
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="not in team's allowed models\\. Team allowed models") as exc_info:
|
||||
_check_team_project_limits(team_object=team, data=data)
|
||||
|
||||
assert "claude-3" in str(exc_info.value.detail)
|
||||
|
|
@ -476,7 +476,7 @@ def test_check_team_project_limits_budget_exceeds_team():
|
|||
max_budget=150.0, # exceeds team's 100.0
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Project max_budget') as exc_info:
|
||||
_check_team_project_limits(team_object=team, data=data)
|
||||
|
||||
assert "exceeds team's max_budget" in str(exc_info.value.detail)
|
||||
|
|
@ -551,7 +551,7 @@ def test_check_team_project_limits_tpm_exceeds_team():
|
|||
tpm_limit=20000, # exceeds team's 10000
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Project tpm_limit') as exc_info:
|
||||
_check_team_project_limits(team_object=team, data=data)
|
||||
|
||||
assert "exceeds team's tpm_limit" in str(exc_info.value.detail)
|
||||
|
|
@ -577,7 +577,7 @@ def test_check_team_project_limits_negative_budget():
|
|||
max_budget=-10.0,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='max_budget cannot be negative\\. Received') as exc_info:
|
||||
_check_team_project_limits(team_object=team, data=data)
|
||||
|
||||
assert "cannot be negative" in str(exc_info.value.detail)
|
||||
|
|
@ -604,7 +604,7 @@ def test_check_team_project_limits_soft_budget_gte_max():
|
|||
soft_budget=100.0, # equal to max, should fail
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='must be strictly lower than max_budget') as exc_info:
|
||||
_check_team_project_limits(team_object=team, data=data)
|
||||
|
||||
assert "must be strictly lower" in str(exc_info.value.detail)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ async def test_dynamoai_blocks_content_with_block_action():
|
|||
guardrail.should_run_guardrail = MagicMock(return_value=True)
|
||||
|
||||
# Test that the guardrail raises ValueError for blocked content
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='violation\\(s\\) detected') as exc_info:
|
||||
await guardrail.async_pre_call_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class TestEUAIActArticle5ConditionalMatching:
|
|||
# Apply guardrail
|
||||
if expected == "BLOCK":
|
||||
# Should raise an exception or return modified response indicating block
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Content blocked: eu_ai_act_article') as exc_info:
|
||||
await content_filter_guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class TestEUAIActFrench3Scenarios:
|
|||
print(f"{'='*70}\n")
|
||||
|
||||
# Should raise an exception (blocked)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="prohibited_practices_fr conditional match 'concevoir \\+") as exc_info:
|
||||
await content_filter_guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
@ -123,7 +123,7 @@ class TestEUAIActFrench3Scenarios:
|
|||
print(f"{'='*70}\n")
|
||||
|
||||
# Should raise an exception (blocked)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="prohibited_practices_fr conditional match 'créer \\+") as exc_info:
|
||||
await content_filter_guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
@ -194,7 +194,7 @@ class TestEUAIActFrench3Scenarios:
|
|||
print(f"{'='*70}\n")
|
||||
|
||||
# Should raise an exception (blocked by conditional matching)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="prohibited_practices_fr conditional match 'développer \\+") as exc_info:
|
||||
await content_filter_guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
@ -278,7 +278,7 @@ class TestFrenchEdgeCases:
|
|||
request_data = {"messages": [{"role": "user", "content": sentence}]}
|
||||
|
||||
# Should still block (no exception bypass)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="prohibited_practices_fr conditional match 'créer \\+ crédit") as exc_info:
|
||||
await content_filter_guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ def _make_guardrail(yaml_filename: str, category_name: str) -> ContentFilterGuar
|
|||
|
||||
async def _expect_block(guardrail: ContentFilterGuardrail, sentence: str, reason: str):
|
||||
request_data = {"messages": [{"role": "user", "content": sentence}]}
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Content blocked: sg_mas_') as exc_info:
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def _make_guardrail(yaml_filename: str, category_name: str) -> ContentFilterGuar
|
|||
async def _expect_block(guardrail: ContentFilterGuardrail, sentence: str, reason: str):
|
||||
"""Assert that the guardrail BLOCKS the sentence."""
|
||||
request_data = {"messages": [{"role": "user", "content": sentence}]}
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Content blocked: sg_pdpa_') as exc_info:
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": [sentence]},
|
||||
request_data=request_data,
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ def test_hashicorp_get_url_rejects_path_traversal(monkeypatch, malicious_secret_
|
|||
monkeypatch.setenv("HCP_VAULT_TOKEN", "test-token-for-get-url-only")
|
||||
manager = HashicorpSecretManager()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Invalid secret_name'):
|
||||
manager.get_url(malicious_secret_name)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2147,7 +2147,7 @@ def test_validate_user_messages_invalid_content_type():
|
|||
|
||||
messages = [{"content": [{"type": "invalid_type", "text": "Hello"}]}]
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match='Please ensure all messages are valid OpenAI chat completion') as e:
|
||||
validate_chat_completion_user_messages(messages)
|
||||
|
||||
assert "Invalid message" in str(e)
|
||||
|
|
|
|||
|
|
@ -37,27 +37,27 @@ def test_validate_tool_choice_cursor_format():
|
|||
def test_validate_tool_choice_invalid_dict():
|
||||
"""Test that invalid dict formats raise exceptions."""
|
||||
# Missing both type and function
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Invalid tool choice, tool_choice=\\{\\}\\. Please ensure') as exc_info:
|
||||
validate_chat_completion_tool_choice({})
|
||||
assert "Invalid tool choice" in str(exc_info.value)
|
||||
|
||||
# Invalid type value
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Invalid tool choice, tool_choice=\\{'type': 'invalid'\\}\\.") as exc_info:
|
||||
validate_chat_completion_tool_choice({"type": "invalid"})
|
||||
assert "Invalid tool choice" in str(exc_info.value)
|
||||
|
||||
# Has type but missing function when type is "function"
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Invalid tool choice, tool_choice=\\{'type': 'function'\\}\\.") as exc_info:
|
||||
validate_chat_completion_tool_choice({"type": "function"})
|
||||
assert "Invalid tool choice" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_validate_tool_choice_invalid_type():
|
||||
"""Test that invalid types raise exceptions."""
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="<class 'int'>\\. Expecting str, or dict\\. Please ensure") as exc_info:
|
||||
validate_chat_completion_tool_choice(123)
|
||||
assert "Got=<class 'int'>" in str(exc_info.value)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Invalid tool choice, tool_choice=\\[\\]\\. Got=<class 'list'>\\.") as exc_info:
|
||||
validate_chat_completion_tool_choice([])
|
||||
assert "Got=<class 'list'>" in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ async def test_responses_streaming_failure_triggers_failure_handlers():
|
|||
call_type=CallTypes.responses.value,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="boom"):
|
||||
iterator._process_chunk('{"delta": "chunk"}')
|
||||
|
||||
# allow failure callbacks to run
|
||||
|
|
|
|||
|
|
@ -1890,7 +1890,7 @@ def test_bedrock_completion_test_4(modify_params):
|
|||
]
|
||||
assert transformed_messages == expected_messages
|
||||
else:
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match=r"litellm\.modify_params") as e:
|
||||
litellm.completion(**data)
|
||||
assert "litellm.modify_params" in str(e.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -982,7 +982,7 @@ def test_convert_to_model_response_object_with_real_error():
|
|||
},
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception) as exc_info: # noqa: PT011 # message rides on .message, str() is empty
|
||||
convert_to_model_response_object(
|
||||
model_response_object=ModelResponse(),
|
||||
response_object=response_object,
|
||||
|
|
@ -1243,7 +1243,7 @@ def test_convert_to_model_response_object_with_error_code_only():
|
|||
},
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as exc_info: # noqa: B017 # bare Exception raised, so status_code is the assertion
|
||||
with pytest.raises(Exception) as exc_info: # noqa: B017, PT011 # bare Exception, empty message, so status_code is the assertion
|
||||
convert_to_model_response_object(
|
||||
model_response_object=ModelResponse(),
|
||||
response_object=response_object,
|
||||
|
|
@ -1423,7 +1423,7 @@ def test_error_message_includes_function_args():
|
|||
"choices": [{"index": 0}],
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='in convert_to_model_response_object') as exc_info:
|
||||
convert_to_model_response_object(
|
||||
model_response_object=ModelResponse(),
|
||||
response_object=response_object,
|
||||
|
|
|
|||
|
|
@ -1845,7 +1845,7 @@ def test_parse_tool_call_arguments_malformed_json():
|
|||
parse_tool_call_arguments,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="Failed to parse tool call arguments for tool 'load_skill") as exc_info:
|
||||
parse_tool_call_arguments(
|
||||
'{"skill_name": "pptx',
|
||||
tool_name="load_skill",
|
||||
|
|
@ -1877,7 +1877,7 @@ def test_convert_to_anthropic_tool_invoke_malformed_json():
|
|||
}
|
||||
]
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="Failed to parse tool call arguments for tool 'bad_tool") as exc_info:
|
||||
convert_to_anthropic_tool_invoke(tool_calls)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
|
|
@ -2023,7 +2023,7 @@ def test_parse_tool_call_arguments_still_raises_for_unrepairable():
|
|||
parse_tool_call_arguments,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="Failed to parse tool call arguments for tool 'test_tool") as exc_info:
|
||||
parse_tool_call_arguments(
|
||||
'{"key": "unterminated',
|
||||
tool_name="test_tool",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def test_split_embedding_by_shape_fails_with_shape_value_error():
|
|||
"data": [1, 2, 3, 4, 5, 6],
|
||||
}
|
||||
]
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Shape must be of length'):
|
||||
TritonEmbeddingConfig.split_embedding_by_shape(
|
||||
data[0]["data"], data[0]["shape"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ def test_transform_request_invalid_provider(bedrock_transformer):
|
|||
"""Test request transformation with invalid provider"""
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Bedrock Invoke HTTPX: Unknown provider=None') as exc_info:
|
||||
bedrock_transformer.transform_request(
|
||||
model="invalid.model",
|
||||
messages=messages,
|
||||
|
|
|
|||
|
|
@ -264,10 +264,6 @@ def test_get_end_user_id_from_request_body_backwards_compatibility():
|
|||
["gpt-3.5-turbo", "gpt-4o-mini-general-deployment"],
|
||||
),
|
||||
({"model": "gpt-3.5-turbo"}, "gpt-3.5-turbo"),
|
||||
(
|
||||
{"model": "gpt-3.5-turbo, gpt-4o-mini-general-deployment"},
|
||||
["gpt-3.5-turbo", "gpt-4o-mini-general-deployment"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_model_from_request(request_data, expected_model):
|
||||
|
|
|
|||
|
|
@ -1433,7 +1433,7 @@ async def test_exception_bubbling_up(sync_mode, stream_mode, model):
|
|||
sync_stream=sync_mode,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='litellm\\.BadRequestError: OpenAIException - Invalid value') as exc_info:
|
||||
await _call_with_bad_role()
|
||||
|
||||
assert exc_info.value.code == "invalid_value"
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ class TestFileConsts:
|
|||
def test_get_file_extension_from_mime_type(self):
|
||||
assert get_file_extension_from_mime_type("audio/aac") == "aac"
|
||||
assert get_file_extension_from_mime_type("application/pdf") == "pdf"
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Unknown extension for mime type: application'):
|
||||
get_file_extension_from_mime_type("application/unknown")
|
||||
|
||||
def test_get_file_type_from_extension(self):
|
||||
assert get_file_type_from_extension("aac") == FileType.AAC
|
||||
assert get_file_type_from_extension("pdf") == FileType.PDF
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Unknown file type for extension: unknown'):
|
||||
get_file_type_from_extension("unknown")
|
||||
|
||||
def test_get_file_extension_for_file_type(self):
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ def test_get_model_info_bedrock_region():
|
|||
"ft:gpt-3.5-turbo:my-org:custom_suffix:id",
|
||||
"ft:gpt-4-0613:my-org:custom_suffix:id",
|
||||
"ft:davinci-002:my-org:custom_suffix:id",
|
||||
"ft:gpt-4-0613:my-org:custom_suffix:id",
|
||||
"ft:babbage-002:my-org:custom_suffix:id",
|
||||
"gpt-35-turbo",
|
||||
"ada",
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ async def test_provider_budgets_e2e_test_expect_to_fail():
|
|||
await asyncio.sleep(2.5)
|
||||
|
||||
for _ in range(3):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Exceeded budget for provider") as exc_info:
|
||||
await router.acompletion(
|
||||
messages=[{"role": "user", "content": "Hello, how are you?"}],
|
||||
model="anthropic/claude-sonnet-4-5-20250929",
|
||||
|
|
@ -594,7 +594,7 @@ async def test_deployment_budgets_e2e_test_expect_to_fail():
|
|||
await asyncio.sleep(2.5)
|
||||
|
||||
for _ in range(3):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Exceeded budget for deployment") as exc_info:
|
||||
await router.acompletion(
|
||||
messages=[{"role": "user", "content": "Hello, how are you?"}],
|
||||
model="openai/gpt-4o-mini",
|
||||
|
|
@ -646,7 +646,7 @@ async def test_tag_budgets_e2e_test_expect_to_fail():
|
|||
await asyncio.sleep(2.5)
|
||||
|
||||
for _ in range(3):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match=f"Exceeded budget for tag='{TAG_NAME}'") as exc_info:
|
||||
await router.acompletion(
|
||||
messages=[{"role": "user", "content": "Hello, how are you?"}],
|
||||
model="openai/gpt-4o-mini",
|
||||
|
|
|
|||
|
|
@ -1430,7 +1430,7 @@ async def test_router_fallbacks_default_and_model_specific_fallbacks(sync_mode):
|
|||
messages=[{"role": "user", "content": "Hey, how's it going?"}],
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='litellm\\.AuthenticationError: AuthenticationError') as exc_info:
|
||||
await _call_bad_model()
|
||||
assert isinstance(
|
||||
exc_info.value, litellm.AuthenticationError
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ def test_cleanup_timestamps():
|
|||
assert all(isinstance(x, float) for x in result)
|
||||
|
||||
# Test invalid input
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="start_time is required, got=invalid of type <class 'str'>"):
|
||||
StandardLoggingPayloadSetup.cleanup_timestamps(
|
||||
"invalid", end_float, completion_float
|
||||
)
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ async def test_team_blocking_behavior_multi_instance():
|
|||
assert team_info_4001["blocked"] is True, "Team should be blocked after update"
|
||||
|
||||
# 8. Make a chat completion request on port 4000 with a new prompt; expect it to be blocked.
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match="(?i)blocked") as excinfo:
|
||||
await chat_completion_on_port(
|
||||
session,
|
||||
key=key,
|
||||
|
|
@ -157,7 +157,7 @@ async def test_team_blocking_behavior_multi_instance():
|
|||
), f"Expected error indicating team blocked, got: {error_msg}"
|
||||
|
||||
# 9. Make a chat completion request on port 4000 with a new prompt; expect it to be blocked.
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match="(?i)blocked") as excinfo:
|
||||
await chat_completion_on_port(
|
||||
session,
|
||||
key=key,
|
||||
|
|
@ -171,7 +171,7 @@ async def test_team_blocking_behavior_multi_instance():
|
|||
), f"Expected error indicating team blocked, got: {error_msg}"
|
||||
|
||||
# 9. Repeat the chat completion request with another new prompt; expect it to be blocked.
|
||||
with pytest.raises(Exception) as excinfo_second:
|
||||
with pytest.raises(Exception, match="(?i)blocked") as excinfo_second:
|
||||
await chat_completion_on_port(
|
||||
session,
|
||||
key=key,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class TestAzureDocumentIntelligencePagesParam:
|
|||
cfg.map_ocr_params({"pages": [True, False]}, {}, "prebuilt-layout")
|
||||
|
||||
def test_map_ocr_params_unsupported_type_raises(self, cfg):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='based, Mistral-style\\) or a string like'):
|
||||
cfg.map_ocr_params({"pages": 5}, {}, "prebuilt-layout")
|
||||
|
||||
def test_get_complete_url_appends_pages_query(self, cfg):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import asyncio
|
|||
import aiohttp
|
||||
import json
|
||||
from httpx import AsyncClient
|
||||
from openai import PermissionDeniedError
|
||||
from typing import Any, Optional, List, Literal
|
||||
|
||||
|
||||
|
|
@ -134,7 +135,7 @@ async def test_model_access_update():
|
|||
await mock_chat_completion(session=session, key=key, model="openai/gpt-5.5")
|
||||
|
||||
# Should fail with gpt-5-mini
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(PermissionDeniedError) as exc_info:
|
||||
await mock_chat_completion(
|
||||
session=session, key=key, model="openai/gpt-5-mini"
|
||||
)
|
||||
|
|
@ -157,7 +158,7 @@ async def test_model_access_update():
|
|||
)
|
||||
|
||||
# Non-OpenAI model should still fail
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(PermissionDeniedError) as exc_info:
|
||||
await mock_chat_completion(
|
||||
session=session, key=key, model="anthropic/claude-2"
|
||||
)
|
||||
|
|
@ -254,7 +255,7 @@ async def test_team_model_access_update():
|
|||
await mock_chat_completion(session=session, key=key, model="openai/gpt-5.5")
|
||||
|
||||
# Should fail with gpt-5-mini
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(PermissionDeniedError) as exc_info:
|
||||
await mock_chat_completion(
|
||||
session=session, key=key, model="openai/gpt-5-mini"
|
||||
)
|
||||
|
|
@ -279,7 +280,7 @@ async def test_team_model_access_update():
|
|||
)
|
||||
|
||||
# Non-OpenAI model should still fail
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(PermissionDeniedError) as exc_info:
|
||||
await mock_chat_completion(
|
||||
session=session, key=key, model="anthropic/claude-2"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1340,6 +1340,6 @@ async def test_team_model_alias(prisma_client, requested_model, should_pass):
|
|||
}, "Expected model aliases to be present"
|
||||
else:
|
||||
# Verify the key fails with non-aliased models
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await user_api_key_auth(request=request, api_key=f"Bearer {generated_key}")
|
||||
assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from litellm._uuid import uuid
|
|||
from datetime import datetime
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import Request
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
load_dotenv()
|
||||
|
|
@ -530,7 +530,7 @@ async def test_user_role_permissions(prisma_client, route, user_role, expected_r
|
|||
print(f"Auth passed as expected for {route} with role {user_role}")
|
||||
else:
|
||||
# Should raise an error
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises((ProxyException, HTTPException)) as exc_info:
|
||||
await user_api_key_auth(request=request, api_key=bearer_token)
|
||||
print(f"Auth failed as expected for {route} with role {user_role}")
|
||||
print(f"Error message: {str(exc_info.value)}")
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ async def test_can_key_call_model(model, expect_to_work):
|
|||
if expect_to_work:
|
||||
await can_key_call_model(**args)
|
||||
else:
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match='key not allowed to access model\\. This key can only access') as e:
|
||||
await can_key_call_model(**args)
|
||||
|
||||
print(e)
|
||||
|
|
@ -958,7 +958,7 @@ async def test_can_key_call_model_with_aliases(model, alias_map, expect_to_work)
|
|||
llm_router=router,
|
||||
)
|
||||
else:
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match='key not allowed to access model\\. This key can only access') as e:
|
||||
await can_key_call_model(
|
||||
model=model,
|
||||
llm_model_list=llm_model_list,
|
||||
|
|
|
|||
|
|
@ -1583,7 +1583,7 @@ async def test_auth_jwt_mismatched_key_fails(monkeypatch):
|
|||
|
||||
h = JWTHandler()
|
||||
with patch.object(h, "get_public_key", new=AsyncMock(return_value=rsa_jwk)):
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='Validation fails: Expecting a PEM-formatted key\\.') as exc:
|
||||
await h.auth_jwt(token)
|
||||
assert "Validation fails" in str(exc.value)
|
||||
|
||||
|
|
@ -1826,7 +1826,7 @@ async def test_multi_issuer_jwt_unknown_issuer_without_global_jwks_rejected(
|
|||
kid="issuer-key",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='Missing JWT Public Key URL from environment\\.') as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
|
||||
assert "Missing JWT Public Key URL" in str(exc.value)
|
||||
|
|
@ -1857,7 +1857,7 @@ async def test_multi_issuer_jwt_rejects_wrong_audience(monkeypatch):
|
|||
kid="issuer-key",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match="Validation fails: Audience doesn't match") as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
|
||||
assert "Validation fails" in str(exc.value)
|
||||
|
|
@ -1900,7 +1900,7 @@ async def test_multi_issuer_jwt_same_kid_does_not_cross_issuer_keys(monkeypatch)
|
|||
kid=shared_kid,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='Validation fails: Signature verification failed') as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
|
||||
assert "Validation fails" in str(exc.value)
|
||||
|
|
@ -1953,7 +1953,7 @@ def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
|||
issuer = "https://issuer.example.com"
|
||||
jwks_url = f"{issuer}/keys"
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='must configure audience or set') as exc:
|
||||
LiteLLM_JWTAuth(
|
||||
issuers=[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1026,7 +1026,7 @@ def test_enforced_params_check(
|
|||
from litellm.proxy.litellm_pre_call_utils import _enforced_params_check
|
||||
|
||||
if expected_error:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='in request body\\. This is a required param'):
|
||||
_enforced_params_check(
|
||||
request_body=request_body,
|
||||
general_settings=general_settings,
|
||||
|
|
@ -2626,7 +2626,7 @@ async def test_during_call_hook_parallel_execution_with_error():
|
|||
try:
|
||||
litellm.callbacks = [FailingGuardrail()]
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Guardrail violation detected!') as exc_info:
|
||||
await proxy_logging.during_call_hook(
|
||||
data={
|
||||
"model": "gpt-4",
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ async def test_update_spend_logs_non_connection_error():
|
|||
prisma_client.db.litellm_spendlogs.create_many = create_many_mock
|
||||
|
||||
# Execute and verify it raises immediately without retrying
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Unexpected database error') as exc_info:
|
||||
await update_spend(prisma_client, None, proxy_logging_obj)
|
||||
|
||||
# Verify error message
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def test_routing_strategy_init_invalid_strategy(model_list):
|
|||
router = Router(model_list=model_list)
|
||||
|
||||
# Test common mistake: "simple" instead of "simple-shuffle"
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="usage-based-routing', 'provider-budget-routing'\\]\\. Check") as exc_info:
|
||||
router.routing_strategy_init(
|
||||
routing_strategy="simple", routing_strategy_args={}
|
||||
)
|
||||
|
|
@ -106,7 +106,7 @@ def test_routing_strategy_init_invalid_strategy(model_list):
|
|||
assert "Router SDK" in error_msg
|
||||
|
||||
# Test completely invalid strategy
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="usage-based-routing', 'provider-budget-routing'\\]\\. Check") as exc_info:
|
||||
router.routing_strategy_init(
|
||||
routing_strategy="not-a-real-strategy", routing_strategy_args={}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ def test_validate_mcp_server_name_direct():
|
|||
validate_mcp_server_name("valid name")
|
||||
|
||||
# Test that invalid names with hyphens raise exceptions
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="Server name cannot contain '-'\\. Use an alternative") as exc_info:
|
||||
validate_mcp_server_name("invalid-name")
|
||||
assert "cannot contain" in str(exc_info.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ async def test_send_email_missing_api_key():
|
|||
try:
|
||||
logger = SendGridEmailLogger()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='SENDGRID_API_KEY is not set'):
|
||||
await logger.send_email(
|
||||
from_email="test@example.com",
|
||||
to_email=["recipient@example.com"],
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ async def test_afile_content_error_reports_unified_id_not_provider_uri():
|
|||
mock_router.get_deployment_credentials_with_provider = MagicMock(return_value=None)
|
||||
mock_router.afile_content = AsyncMock(side_effect=Exception("deployment failed"))
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='LiteLLM Managed File object with') as exc_info:
|
||||
await managed_files.afile_content(
|
||||
file_id=unified_file_id,
|
||||
litellm_parent_otel_span=None,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ async def test_get_usage_data_rejects_invalid_limit(monkeypatch: pytest.MonkeyPa
|
|||
"""limit must coerce to int or raise ValueError before hitting the DB."""
|
||||
db, query_mock = _setup_db(monkeypatch, [])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='limit must be an integer'):
|
||||
await db.get_usage_data(limit="invalid")
|
||||
|
||||
assert query_mock.await_count == 0
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class TestCloudZeroStreamer:
|
|||
"""Test _parse_and_convert_timestamp method with invalid timestamp."""
|
||||
streamer = CloudZeroStreamer("test-key", "test-connection")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Could not parse timestamp 'invalid-timestamp': Invalid"):
|
||||
streamer._parse_and_convert_timestamp("invalid-timestamp")
|
||||
|
||||
def test_prepare_batch_payload(self):
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ async def test_should_accept_string_timestamps(monkeypatch: pytest.MonkeyPatch):
|
|||
async def test_should_reject_invalid_limit(monkeypatch: pytest.MonkeyPatch):
|
||||
db, query_mock = _setup_db(monkeypatch, [])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='limit must be an integer'):
|
||||
await db.get_usage_data(limit="invalid")
|
||||
|
||||
assert query_mock.await_count == 0
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ def _window(freq: str = "hourly", hour: int = 5) -> FocusTimeWindow:
|
|||
|
||||
|
||||
def test_should_require_bucket_name():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='bucket_name must be provided for S'):
|
||||
FocusS3Destination(prefix="focus", config={})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -95,9 +95,9 @@ def enc_project(p): # how client encodes project in urls
|
|||
# Constructor / config tests
|
||||
# -----------------------------
|
||||
def test_init_requires_project_and_token():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='project and access_token are required'):
|
||||
GitLabClient({"project": "p"})
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='project and access_token are required'):
|
||||
GitLabClient({"access_token": "t"})
|
||||
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ def test_set_ref_updates_effective_ref():
|
|||
c = make_client(branch="main")
|
||||
c.set_ref("feature/x")
|
||||
assert c.ref == "feature/x"
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='ref must be a non-empty string'):
|
||||
c.set_ref("")
|
||||
|
||||
|
||||
|
|
@ -193,12 +193,12 @@ def test_get_file_content_permission_errors_are_mapped():
|
|||
raw_url = f"https://gitlab.example.com/api/v4/projects/{enc_project('group/sub/repo')}/repository/files/secure%2Ffile.prompt/raw?ref=main"
|
||||
# raise_for_status will be called, so return 403 response (not an exception from transport)
|
||||
c.http_handler.routes[raw_url] = FakeResponse(status_code=403)
|
||||
with pytest.raises(Exception) as ei:
|
||||
with pytest.raises(Exception, match="Check your GitLab permissions for project 'group") as ei:
|
||||
c.get_file_content("secure/file.prompt")
|
||||
assert "Access denied" in str(ei.value)
|
||||
|
||||
c.http_handler.routes[raw_url] = FakeResponse(status_code=401)
|
||||
with pytest.raises(Exception) as ei2:
|
||||
with pytest.raises(Exception, match='Authentication failed\\. Check your GitLab token and') as ei2:
|
||||
c.get_file_content("secure/file.prompt")
|
||||
assert "Authentication failed" in str(ei2.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ class TestLevoIntegration(unittest.TestCase):
|
|||
"""Test health check returns unhealthy status when required vars are missing."""
|
||||
# Try to create logger without required env vars
|
||||
# This should fail during config, but we can test health check logic
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='LEVOAI_API_KEY environment variable is required for Levo'):
|
||||
LevoLogger.get_levo_config()
|
||||
|
||||
@patch.dict(
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@ def test_token_type_rejected_from_either_list(attributes, monkeypatch):
|
|||
recorder rather than silently ignored, so the misconfig is caught at all."""
|
||||
recorder = _recorder(monkeypatch, attributes)
|
||||
kwargs, response_obj, start, end = _build_call()
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='otel\\.attributes: gen_ai\\.token\\.type is a structural') as exc_info:
|
||||
recorder.record(kwargs, response_obj, start, end)
|
||||
# The dedicated discriminator guard, not the generic unknown-name path: assert
|
||||
# the specific reason so dropping that guard (and falling through to "unknown
|
||||
|
|
|
|||
|
|
@ -1163,7 +1163,7 @@ def test_max_langfuse_clients_limit():
|
|||
assert litellm.initialized_langfuse_clients == 2
|
||||
|
||||
# Third client should fail with exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Max langfuse clients reached') as exc_info:
|
||||
logger3 = LangFuseLogger(
|
||||
langfuse_public_key="test_key_3",
|
||||
langfuse_secret="test_secret_3",
|
||||
|
|
|
|||
|
|
@ -1169,7 +1169,7 @@ def test_bedrock_image_processor_content_type_fallback_failure():
|
|||
# Test with URL without recognizable extension
|
||||
image_url = "https://example.com/unknown-file"
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
with pytest.raises(ValueError, match='Unable to determine content type from URL: https') as excinfo:
|
||||
BedrockImageProcessor._post_call_image_processing(mock_response, image_url)
|
||||
|
||||
assert "Unable to determine content type" in str(excinfo.value)
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ def test_top_level_kwargs_overrides_metadata_slots():
|
|||
def test_env_reference_at_top_level_raises_with_guidance():
|
||||
kwargs = {"langfuse_public_key": "os.environ/LANGFUSE_PUBLIC_KEY"}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="Callback param 'langfuse_public_key' \\(from request body\\)") as exc_info:
|
||||
initialize_standard_callback_dynamic_params(kwargs)
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
@ -127,7 +127,7 @@ def test_env_reference_in_metadata_raises_with_guidance():
|
|||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="Callback param 'langsmith_api_key' \\(from metadata\\) contains") as exc_info:
|
||||
initialize_standard_callback_dynamic_params(kwargs)
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def test_parse_json_verdict_tolerates_fences_and_prose(raw, expected):
|
|||
|
||||
|
||||
def test_parse_json_verdict_rejects_non_object():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='judge response is not a JSON object'):
|
||||
parse_json_verdict('["not", "an", "object"]')
|
||||
with pytest.raises((json.JSONDecodeError, ValueError)):
|
||||
parse_json_verdict("no json here at all")
|
||||
|
|
|
|||
|
|
@ -982,7 +982,7 @@ async def test_bedrock_validation_error_raises_directly(logging_obj: Logging):
|
|||
make_call=_raise_400,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match='litellm\\.BadRequestError: BedrockException') as excinfo:
|
||||
await response.__anext__()
|
||||
assert not isinstance(excinfo.value, MidStreamFallbackError)
|
||||
assert getattr(excinfo.value, "status_code", None) == 400
|
||||
|
|
@ -2722,7 +2722,7 @@ def test_dispatch_text_completion_codestral_requires_string(
|
|||
is a programming error and must surface loudly."""
|
||||
initialized_custom_stream_wrapper.custom_llm_provider = "text-completion-codestral"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="chunk is not a string: \\{'not': 'a string'\\}"):
|
||||
_run_dispatch(initialized_custom_stream_wrapper, {"not": "a string"})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -763,24 +763,6 @@ class TestTokenizerSelection(unittest.TestCase):
|
|||
],
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "These are some sample images from a movie. Based on these images, what do you think the tone of the movie is?",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"image_url": {
|
||||
"url": "https://gratisography.com/wp-content/uploads/2024/11/gratisography-augmented-reality-800x525.jpg",
|
||||
"detail": "high",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_bad_input_token_counter(model, messages):
|
||||
|
|
@ -1174,7 +1156,7 @@ def test_count_content_list_rejects_unknown_type():
|
|||
"""
|
||||
from litellm.litellm_core_utils.token_counter import _count_content_list
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Error getting number of tokens from content list: Invalid') as exc_info:
|
||||
_count_content_list(
|
||||
count_function=len,
|
||||
content_list=[{"type": "totally_unknown_block"}],
|
||||
|
|
|
|||
|
|
@ -100,12 +100,12 @@ class TestEncodeUrlPathSegment:
|
|||
|
||||
@pytest.mark.parametrize("value", ["", ".", "..", None])
|
||||
def test_rejects_empty_and_dot_segments(self, value):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="resource_id (is required|cannot be a dot path segment)"):
|
||||
encode_url_path_segment(value, field_name="resource_id")
|
||||
|
||||
@pytest.mark.parametrize("value", ["../model", "model/../other", "/model"])
|
||||
def test_rejects_dot_segments_in_multi_segment_paths(self, value):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="model (is required|cannot be a dot path segment)"):
|
||||
encode_url_path_segments(value, field_name="model")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ def test_flux_style_request_still_remaps_to_legacy_fields():
|
|||
|
||||
|
||||
def test_openai_style_unsupported_param_raises_without_drop_params():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Supported parameters are'):
|
||||
AimlImageGenerationConfig().map_openai_params(
|
||||
non_default_params={"image_size": {"width": 1024, "height": 1024}},
|
||||
optional_params={},
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def test_anthropic_messages_handler_skips_the_gateway_on_recursion():
|
|||
"litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp",
|
||||
new=AsyncMock(return_value={"routed": True}),
|
||||
) as routed:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='anthropic_messages_handler is not implemented for sync calls'):
|
||||
anthropic_messages_handler(
|
||||
max_tokens=100,
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
|
|
@ -80,7 +80,7 @@ def test_anthropic_messages_handler_leaves_native_tools_alone():
|
|||
"litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp",
|
||||
new=AsyncMock(return_value={"routed": True}),
|
||||
) as routed:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='anthropic_messages_handler is not implemented for sync calls'):
|
||||
anthropic_messages_handler(
|
||||
max_tokens=100,
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class TestAzureAIRerankConfigGetCompleteUrl:
|
|||
self.model = "azure_ai/cohere-rerank-v3-english"
|
||||
|
||||
def test_api_base_required(self):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Azure AI API Base is required\\. api_base=None\\. Set in') as exc_info:
|
||||
self.config.get_complete_url(api_base=None, model=self.model)
|
||||
|
||||
assert "api_base=None" in str(exc_info.value)
|
||||
|
|
@ -31,7 +31,7 @@ class TestAzureAIRerankConfigGetCompleteUrl:
|
|||
],
|
||||
)
|
||||
def test_api_base_requires_scheme(self, api_base):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Azure AI API Base must be an absolute URL including scheme') as exc_info:
|
||||
self.config.get_complete_url(api_base=api_base, model=self.model)
|
||||
|
||||
error_message = str(exc_info.value).lower()
|
||||
|
|
|
|||
|
|
@ -1944,7 +1944,7 @@ def test_role_assumption_access_denied_raises_when_different_role():
|
|||
with patch.object(
|
||||
base_aws_llm, "_is_already_running_as_role", return_value=False
|
||||
):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='An error occurred \\(AccessDenied\\) when calling the') as exc_info:
|
||||
base_aws_llm._auth_with_aws_role(
|
||||
aws_access_key_id=None,
|
||||
aws_secret_access_key=None,
|
||||
|
|
@ -1969,7 +1969,7 @@ def test_role_assumption_non_access_denied_error_propagated():
|
|||
)
|
||||
|
||||
with patch("boto3.client", return_value=mock_sts_client):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='An error occurred \\(MalformedPolicyDocument\\) when calling') as exc_info:
|
||||
base_aws_llm._auth_with_aws_role(
|
||||
aws_access_key_id=None,
|
||||
aws_secret_access_key=None,
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class TestBedrockMantleResponsesURL:
|
|||
monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False)
|
||||
monkeypatch.delenv("AWS_REGION", raising=False)
|
||||
cfg = BedrockMantleResponsesAPIConfig()
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="api\\.aws\\.attacker\\.example/'\\. Region names must contain only"):
|
||||
cfg.get_complete_url(
|
||||
api_base=None,
|
||||
litellm_params={
|
||||
|
|
@ -1418,7 +1418,7 @@ class TestBedrockMantleResponsesSigV4:
|
|||
signer.get_credentials = MagicMock(side_effect=NoCredentialsError())
|
||||
cfg = BedrockMantleResponsesAPIConfig(aws_signer=signer)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Bedrock Mantle auth failed: no Bearer token and no usable') as exc:
|
||||
cfg.sign_request(
|
||||
headers={},
|
||||
optional_params={"aws_region_name": "us-east-2"},
|
||||
|
|
@ -1448,7 +1448,7 @@ class TestBedrockMantleResponsesSigV4:
|
|||
signer.get_credentials = MagicMock(side_effect=cred_error)
|
||||
cfg = BedrockMantleResponsesAPIConfig(aws_signer=signer)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Bedrock Mantle auth failed: no Bearer token and no usable') as exc:
|
||||
cfg.sign_request(
|
||||
headers={},
|
||||
optional_params={"aws_region_name": "us-east-2"},
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class TestBedrockMantleConfig:
|
|||
monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False)
|
||||
monkeypatch.delenv("AWS_REGION", raising=False)
|
||||
cfg = BedrockMantleChatConfig()
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="api\\.aws\\.attacker\\.example/'\\. Region names must contain only"):
|
||||
cfg._get_openai_compatible_provider_info(
|
||||
None,
|
||||
None,
|
||||
|
|
@ -416,7 +416,7 @@ class TestBedrockMantleChatAuth:
|
|||
signer.get_credentials = MagicMock(side_effect=NoCredentialsError())
|
||||
cfg = BedrockMantleChatConfig(aws_signer=signer)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Bedrock Mantle auth failed: no Bearer token and no usable') as exc:
|
||||
cfg.sign_request(
|
||||
headers={},
|
||||
optional_params={"aws_region_name": "us-east-2"},
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class TestBytezChatConfig:
|
|||
config = BytezChatConfig()
|
||||
headers = {}
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match='Missing api_key, make sure you pass in your api key') as excinfo:
|
||||
config.validate_environment(
|
||||
headers=headers,
|
||||
model=TEST_MODEL,
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ class TestDeepinfraRerankTransform:
|
|||
status_code = 401
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Authentication failed') as exc_info:
|
||||
self.config.get_error_class(error_message, status_code, headers)
|
||||
|
||||
# The method should raise a BaseLLMException
|
||||
|
|
@ -271,7 +271,7 @@ class TestDeepinfraRerankTransform:
|
|||
status_code = 404
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Model not found') as exc_info:
|
||||
self.config.get_error_class(error_message, status_code, headers)
|
||||
|
||||
# Should extract the nested error message
|
||||
|
|
@ -284,7 +284,7 @@ class TestDeepinfraRerankTransform:
|
|||
status_code = 503
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Service unavailable') as exc_info:
|
||||
self.config.get_error_class(error_message, status_code, headers)
|
||||
|
||||
# Should extract the string detail
|
||||
|
|
@ -296,7 +296,7 @@ class TestDeepinfraRerankTransform:
|
|||
status_code = 500
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Invalid JSON error message') as exc_info:
|
||||
self.config.get_error_class(error_message, status_code, headers)
|
||||
|
||||
# Should use the original error message when JSON parsing fails
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ def test_response_format_is_ignored():
|
|||
|
||||
|
||||
def test_unsupported_param_raises_without_drop_params():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Supported parameters are \\['n', 'response_format', 'size'\\]\\."):
|
||||
FalAINanoBananaConfig().map_openai_params(
|
||||
non_default_params={"style": "vivid"},
|
||||
optional_params={},
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class TestFeatherlessAIConfig:
|
|||
"""Test error handling when API key is missing"""
|
||||
config = FeatherlessAIConfig()
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
with pytest.raises(ValueError, match='Missing Featherless AI API Key') as excinfo:
|
||||
config.validate_environment(
|
||||
headers={},
|
||||
model="featherless-ai/Qwerky-72B",
|
||||
|
|
@ -112,7 +112,7 @@ class TestFeatherlessAIConfig:
|
|||
"tool_choice": {"type": "function", "function": {"name": "get_weather"}}
|
||||
}
|
||||
optional_params = {}
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match="litellm\\.UnsupportedParamsError: Featherless AI doesn't") as excinfo:
|
||||
config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
|
|
@ -138,7 +138,7 @@ class TestFeatherlessAIConfig:
|
|||
assert "tools" not in result
|
||||
|
||||
# Test with tools and drop_params=False
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match="litellm\\.UnsupportedParamsError: Featherless AI doesn't") as excinfo:
|
||||
config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ class TestFireworksAIRerankTransform:
|
|||
mock_logging = MagicMock()
|
||||
model_response = RerankResponse()
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Failed to parse response: Invalid JSON: line') as exc_info:
|
||||
self.config.transform_rerank_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ class TestGeminiImageEditTransformation:
|
|||
def test_transform_image_edit_request_without_image_raises(self) -> None:
|
||||
optional_params = {}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Gemini image edit requires at least one image\\.'):
|
||||
self.config.transform_image_edit_request(
|
||||
model=self.model,
|
||||
prompt=self.prompt,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def test_gemini_completion_no_api_key():
|
|||
del os.environ[key]
|
||||
|
||||
# Test without mock_response to ensure actual API key validation
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='in _complete_vertex_ai_beta') as exc_info:
|
||||
completion(
|
||||
model="gemini/gemini-1.5-flash",
|
||||
messages=[{"role": "user", "content": "Test message"}],
|
||||
|
|
@ -60,7 +60,7 @@ def test_gemini_completion_no_api_key_with_mock():
|
|||
with patch("litellm.get_secret") as mock_get_secret:
|
||||
mock_get_secret.return_value = None
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='in _complete_vertex_ai_beta') as exc_info:
|
||||
completion(
|
||||
model="gemini/gemini-1.5-flash",
|
||||
messages=[{"role": "user", "content": "Test message"}],
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class TestHostedVLLMRerankTransform:
|
|||
)
|
||||
assert url2 == "https://api.example.com/rerank"
|
||||
# Raises if api_base is None
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='api_base must be provided for Hosted VLLM rerank'):
|
||||
self.config.get_complete_url(None, self.model)
|
||||
|
||||
def test_transform_response(self):
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def test_langflow_config_get_complete_url():
|
|||
|
||||
def test_langflow_config_get_complete_url_requires_api_base():
|
||||
config = LangFlowConfig()
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='api_base is required for LangFlow\\. Set it via'):
|
||||
config.get_complete_url(
|
||||
api_base=None,
|
||||
api_key=None,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class TestModelScopeImageGenerationTransformation:
|
|||
mock_get_secret.return_value = None
|
||||
headers = {}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='MODELSCOPE_API_KEY is not set\\. Please set it via') as exc_info:
|
||||
self.config.validate_environment(
|
||||
headers=headers,
|
||||
model=self.model,
|
||||
|
|
@ -367,7 +367,7 @@ class TestModelScopeImageGenerationTransformation:
|
|||
|
||||
model_response = ImageResponse(data=[])
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='litellm\\.BadRequestError: ModelScope error: Invalid prompt') as exc_info:
|
||||
self.config.transform_image_generation_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
@ -393,7 +393,7 @@ class TestModelScopeImageGenerationTransformation:
|
|||
|
||||
model_response = ImageResponse(data=[])
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='litellm\\.InternalServerError: Error parsing ModelScope') as exc_info:
|
||||
self.config.transform_image_generation_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class TestNovitaConfig:
|
|||
"""Test error handling when API key is missing"""
|
||||
config = NovitaConfig()
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
with pytest.raises(ValueError, match='Missing Novita AI API Key - A call is being made to novita') as excinfo:
|
||||
config.validate_environment(
|
||||
headers={},
|
||||
model="novita/meta-llama/llama-3.3-70b-instruct",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class TestOCIChatConfig:
|
|||
config = OCIChatConfig()
|
||||
headers = {}
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match='Missing required parameters: oci_user, oci_fingerprint') as excinfo:
|
||||
config.validate_environment(
|
||||
headers=headers,
|
||||
model=TEST_MODEL,
|
||||
|
|
@ -272,7 +272,7 @@ class TestOCIChatConfig:
|
|||
"oci_serving_mode": "INVALID_MODE",
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with pytest.raises(Exception, match="kwarg `oci_serving_mode` must be either 'ON_DEMAND' or") as excinfo:
|
||||
config.transform_request(
|
||||
model=TEST_MODEL_NAME,
|
||||
messages=TEST_MESSAGES, # type: ignore
|
||||
|
|
@ -892,7 +892,7 @@ class TestOCISignerSupport:
|
|||
|
||||
optional_params = {"oci_signer": MockSigner(), "method": "INVALID"}
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
with pytest.raises(ValueError, match='Unsupported HTTP method: INVALID') as excinfo:
|
||||
config.sign_request(
|
||||
headers={},
|
||||
optional_params=optional_params,
|
||||
|
|
@ -1604,7 +1604,7 @@ class TestOCIKeyNormalization:
|
|||
|
||||
# We can't fully test signing without a real key, but we can verify
|
||||
# the error message indicates the key was processed (not a type error)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='why-can-t-i-import-my-pem-file for more details\\.') as exc_info:
|
||||
sign_with_manual_credentials(
|
||||
headers={},
|
||||
optional_params=optional_params,
|
||||
|
|
@ -1630,7 +1630,7 @@ class TestOCIKeyNormalization:
|
|||
"oci_key": crlf_pem,
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='why-can-t-i-import-my-pem-file for more details\\.') as exc_info:
|
||||
sign_with_manual_credentials(
|
||||
headers={},
|
||||
optional_params=optional_params,
|
||||
|
|
@ -1692,7 +1692,7 @@ class TestOCIValidateEnvironment:
|
|||
|
||||
def test_missing_required_credentials_raises_error(self, config):
|
||||
"""Test that missing required credentials raise an error."""
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Missing required parameters: oci_user, oci_fingerprint') as exc_info:
|
||||
config.validate_environment(
|
||||
headers={},
|
||||
model="oci/xai.grok-3",
|
||||
|
|
@ -1875,7 +1875,7 @@ class TestOCIImageUrlTransformation:
|
|||
}
|
||||
]
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Prop `image_url` must be a string or an object with a `url`') as exc_info:
|
||||
adapt_messages_to_generic_oci_standard(messages)
|
||||
|
||||
assert "image_url" in str(exc_info.value)
|
||||
|
|
@ -1899,7 +1899,7 @@ class TestOCIImageUrlTransformation:
|
|||
}
|
||||
]
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Prop `image_url` must be a string or an object with a `url`') as exc_info:
|
||||
adapt_messages_to_generic_oci_standard(messages)
|
||||
|
||||
assert "image_url" in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class TestPGVectorStoreConfig:
|
|||
litellm_params = GenericLiteLLMParams()
|
||||
headers = {}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='PG Vector API key is required\\. Set PG_VECTOR_API_KEY') as exc_info:
|
||||
config.validate_environment(headers, litellm_params)
|
||||
|
||||
assert "PG Vector API key is required" in str(exc_info.value)
|
||||
|
|
@ -84,7 +84,7 @@ class TestPGVectorStoreConfig:
|
|||
config = PGVectorStoreConfig()
|
||||
litellm_params = {}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='PG Vector API base URL is required\\. Set') as exc_info:
|
||||
config.get_complete_url(None, litellm_params)
|
||||
|
||||
assert "PG Vector API base URL is required" in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ class TestRecraftImageEditTransformation:
|
|||
mock_response.status_code = 500
|
||||
mock_response.headers = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Error transforming image edit response: Invalid JSON: line') as exc_info:
|
||||
self.config.transform_image_edit_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class TestRecraftImageGenerationTransformation:
|
|||
non_default_params = {"n": 2, "unsupported_param": "value"}
|
||||
optional_params = {}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Supported parameters are') as exc_info:
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
|
|
@ -171,7 +171,7 @@ class TestRecraftImageGenerationTransformation:
|
|||
mock_get_secret.return_value = None
|
||||
headers = {}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='RECRAFT_API_KEY is not set') as exc_info:
|
||||
self.config.validate_environment(
|
||||
headers=headers,
|
||||
model=self.model,
|
||||
|
|
@ -248,7 +248,7 @@ class TestRecraftImageGenerationTransformation:
|
|||
|
||||
model_response = ImageResponse(data=[])
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Error transforming image generation response: Invalid JSON') as exc_info:
|
||||
self.config.transform_image_generation_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class TestStabilityImageGenerationConfig:
|
|||
non_default_params = {"unsupported_param": "value"}
|
||||
optional_params = {}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="Supported parameters are \\['n', 'size',") as exc_info:
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
|
|
@ -168,7 +168,7 @@ class TestStabilityImageGenerationConfig:
|
|||
|
||||
def test_validate_environment_raises_without_api_key(self):
|
||||
"""Test that validate_environment raises error without API key"""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='STABILITY_API_KEY is not set\\. Please set it via') as exc_info:
|
||||
self.config.validate_environment(
|
||||
headers={},
|
||||
model="stability/sd3",
|
||||
|
|
@ -251,7 +251,7 @@ class TestStabilityImageGenerationConfig:
|
|||
model_response = ImageResponse(data=[])
|
||||
mock_logging = MagicMock()
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Content was filtered by Stability AI safety systems') as exc_info:
|
||||
self.config.transform_image_generation_response(
|
||||
model="stability/sd3",
|
||||
raw_response=mock_response,
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@ class TestErrorHandling:
|
|||
}
|
||||
}
|
||||
mock_response = _make_mock_response(body, status_code=400)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='TinyFish Search: query is required\\. See https') as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=mock_response, logging_obj=None
|
||||
)
|
||||
|
|
@ -713,7 +713,7 @@ class TestErrorHandling:
|
|||
mock_response = _make_mock_response(
|
||||
body, status_code=429, headers={"Retry-After": "60"}
|
||||
)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='TinyFish Search: rate limit exceeded\\. See https') as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=mock_response, logging_obj=None
|
||||
)
|
||||
|
|
@ -728,7 +728,7 @@ class TestErrorHandling:
|
|||
config = TinyfishSearchConfig()
|
||||
body = {"errors": [{"code": "10000", "message": "Internal"}]}
|
||||
mock_response = _make_mock_response(body, status_code=502)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='TinyFish Search') as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=mock_response, logging_obj=None
|
||||
)
|
||||
|
|
@ -742,7 +742,7 @@ class TestErrorHandling:
|
|||
mock_response = _make_mock_response(
|
||||
json_data=None, status_code=502, text="<html>Bad Gateway</html>"
|
||||
)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='TinyFish Search: <html>Bad Gateway<') as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=mock_response, logging_obj=None
|
||||
)
|
||||
|
|
@ -756,7 +756,7 @@ class TestErrorHandling:
|
|||
mock_response = _make_mock_response(
|
||||
json_data=None, status_code=200, text="not json"
|
||||
)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='TinyFish Search: Expected JSON response, got: not json\\.') as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=mock_response, logging_obj=None
|
||||
)
|
||||
|
|
@ -785,7 +785,7 @@ class TestErrorHandling:
|
|||
# check TinyFish's schema, not their own input.
|
||||
config = TinyfishSearchConfig()
|
||||
mock_response = _make_mock_response({"query": "x"}) # no `results` key
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='validation error for SearchResponse') as exc_info:
|
||||
config.transform_search_response(
|
||||
raw_response=mock_response, logging_obj=None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class TestVertexAIFilesIntegration:
|
|||
# This test ensures the type annotations and error messages include vertex_ai
|
||||
|
||||
# Test that calling with unsupported provider raises appropriate error
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="unsupported_provider' is not a valid LlmProviders") as exc_info:
|
||||
litellm.file_content(
|
||||
file_id="test-file-id",
|
||||
custom_llm_provider="unsupported_provider", # This should fail
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def test_validate_vertex_location_accepts_valid(location):
|
|||
["attacker.example/", "evil.com#", "us.attacker.example", "us/../..", "US", "us_central1", "-us", "", None],
|
||||
)
|
||||
def test_validate_vertex_location_rejects_invalid(location):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="vertex_location is required|Invalid vertex_location format"):
|
||||
validate_vertex_location(location)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class TestVolcengineResponsesAPITransformation:
|
|||
monkeypatch.delenv("ARK_API_KEY", raising=False)
|
||||
monkeypatch.delenv("VOLCENGINE_API_KEY", raising=False)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Volcengine API key is required\\. Set ARK_API_KEY /'):
|
||||
config.validate_environment(headers={}, model="volcengine/demo", litellm_params={})
|
||||
|
||||
def test_unsupported_params_are_dropped_with_extra_body(self):
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ def test_volcengine_embedding_error_scenarios():
|
|||
k: v for k, v in scenario.items() if k != "expected_error_pattern"
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match=f"(?i){scenario['expected_error_pattern']}") as exc_info:
|
||||
litellm.embedding(input=["test"], **test_params)
|
||||
|
||||
# Verify error message contains expected pattern
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ class TestVoyageRerankTransform:
|
|||
mock_logging = MagicMock()
|
||||
model_response = RerankResponse()
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Unauthorized') as exc_info:
|
||||
self.config.transform_rerank_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
@ -248,7 +248,7 @@ class TestVoyageRerankTransform:
|
|||
mock_logging = MagicMock()
|
||||
model_response = RerankResponse()
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Failed to parse response: Invalid JSON response') as exc_info:
|
||||
self.config.transform_rerank_response(
|
||||
model=self.model,
|
||||
raw_response=mock_response,
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ class TestVoyageMultimodalEmbeddings:
|
|||
|
||||
monkeypatch.setattr(module, "get_secret_str", lambda name: None)
|
||||
config = VoyageMultimodalEmbeddingConfig()
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Voyage API key is required for multimodal embeddings\\. Set') as exc_info:
|
||||
config.validate_environment(
|
||||
{}, "voyage-multimodal-3.5", [], {}, {}, api_key=None
|
||||
)
|
||||
|
|
@ -207,7 +207,7 @@ class TestVoyageMultimodalEmbeddings:
|
|||
)
|
||||
|
||||
config = VoyageMultimodalEmbeddingConfig()
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Voyage multimodal embeddings require a non-empty') as exc_info:
|
||||
config._normalize_content_item({"type": "image_url", "image_url": {}})
|
||||
assert "image_url" in str(exc_info.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ def test_responses_config_raises_when_no_key_is_available(monkeypatch):
|
|||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.delenv("XAI_API_KEY", raising=False)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='XAI API key is required\\. Set api_key, litellm\\.xai_key') as exc_info:
|
||||
XAIResponsesAPIConfig().validate_environment({}, "xai/grok-3-mini", None)
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -8185,7 +8185,7 @@ class TestGetUserObjectPermission:
|
|||
return_value=None,
|
||||
),
|
||||
):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="user 'human-dangling' names object_permission_id"):
|
||||
await MCPRequestHandler._get_user_object_permission(auth)
|
||||
|
||||
async def test_no_user_id_places_no_ceiling(self):
|
||||
|
|
|
|||
|
|
@ -2695,13 +2695,6 @@ async def test_token_endpoint_respects_x_forwarded_host():
|
|||
"443",
|
||||
"https://internal.local",
|
||||
),
|
||||
(
|
||||
"http://localhost:4000/",
|
||||
"https",
|
||||
"proxy.example.com",
|
||||
"8443",
|
||||
"https://proxy.example.com:8443",
|
||||
),
|
||||
(
|
||||
"http://localhost:4000/",
|
||||
"https",
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class TestShortPrefixHelpers:
|
|||
assert compute_short_server_prefix("abc") != compute_short_server_prefix("abd")
|
||||
|
||||
def test_short_prefix_requires_server_id(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='compute_short_server_prefix requires a non-empty server_id'):
|
||||
compute_short_server_prefix("")
|
||||
|
||||
def test_flag_defaults_to_false(self):
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ def test_get_experimental_ui_login_jwt_auth_token_invalid(
|
|||
invalid_sso_user_defined_values,
|
||||
):
|
||||
"""Test generating JWT token with missing user role"""
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='User role is required for experimental UI login') as exc_info:
|
||||
ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token(
|
||||
invalid_sso_user_defined_values
|
||||
)
|
||||
|
|
@ -883,7 +883,7 @@ async def test_get_user_object_wraps_db_outage_as_valueerror_preserving_context(
|
|||
mock_cache.async_set_cache = AsyncMock()
|
||||
|
||||
with patch("litellm.proxy.auth.auth_checks._should_check_db", return_value=True):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match="User doesn't exist in db\\.") as exc_info:
|
||||
await get_user_object(
|
||||
user_id="outage-contract-probe-user",
|
||||
prisma_client=mock_prisma_client,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1588,7 +1588,7 @@ class TestCheckCompleteCredentialsBlocksSSRF:
|
|||
"litellm.proxy.auth.auth_utils.validate_url",
|
||||
side_effect=SSRFError(f"blocked: {blocked_url}"),
|
||||
):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='is rejected by the SSRF guard') as exc_info:
|
||||
check_complete_credentials(
|
||||
{
|
||||
"model": "gpt-4",
|
||||
|
|
@ -2144,7 +2144,7 @@ class TestIsRequestBodySafeBlocksEndpointTargetingFields:
|
|||
],
|
||||
)
|
||||
def test_endpoint_targeting_field_in_request_body_is_rejected(self, field):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={"model": "gpt-4", field: "https://attacker.example"},
|
||||
general_settings={},
|
||||
|
|
@ -2165,7 +2165,7 @@ class TestIsRequestBodySafeBlocksEndpointTargetingFields:
|
|||
# on the blocklist into an SSRF / credential-exfil hole. Verify
|
||||
# that supplying an api_key (alongside the banned param) does NOT
|
||||
# bypass the gate — it can only be opened by an admin opt-in.
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"model": "gpt-4",
|
||||
|
|
@ -2722,7 +2722,7 @@ class TestObservabilityCallbackBans:
|
|||
],
|
||||
)
|
||||
def test_observability_field_in_request_body_root_is_rejected(self, field):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={"model": "gpt-4", field: "attacker-value"},
|
||||
general_settings={},
|
||||
|
|
@ -2752,7 +2752,7 @@ class TestObservabilityCallbackBans:
|
|||
# Verifies the metadata walk: a value smuggled inside ``metadata``
|
||||
# or ``litellm_metadata`` is just as dangerous as the same field
|
||||
# at the body root, and must hit the same gate.
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"model": "gpt-4",
|
||||
|
|
@ -2787,7 +2787,7 @@ class TestObservabilityCallbackBans:
|
|||
)
|
||||
|
||||
def test_observability_field_in_litellm_params_metadata_is_rejected(self):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request: turn_off_message_logging is not allowed') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"model": "gpt-4",
|
||||
|
|
@ -2814,7 +2814,7 @@ class TestObservabilityCallbackBans:
|
|||
# the ``isinstance(dict)`` guard.
|
||||
import json
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request: langfuse_host is not allowed in request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"model": "gpt-4",
|
||||
|
|
@ -2887,7 +2887,7 @@ def test_model_level_allow_does_not_skip_subsequent_banned_params(monkeypatch):
|
|||
lambda model, param, request_body_value, llm_router: param == "api_base",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request: langfuse_host is not allowed in request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"model": "gpt-4",
|
||||
|
|
@ -2958,7 +2958,7 @@ class TestPricingInjectionBlocked:
|
|||
],
|
||||
)
|
||||
def test_pricing_field_rejected_by_default(self, field, value):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='Rejected Request') as exc:
|
||||
is_request_body_safe(
|
||||
request_body={"model": "gpt-4", field: value},
|
||||
general_settings={},
|
||||
|
|
|
|||
|
|
@ -2589,7 +2589,7 @@ async def test_find_and_validate_raises_when_required_team_not_found():
|
|||
# Token without team info
|
||||
jwt_token = {"sub": "user-1"}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="No team found in token\\. Checked team_id field 'None' and") as exc_info:
|
||||
await JWTAuthManager.find_and_validate_specific_team_id(
|
||||
jwt_handler=jwt_handler,
|
||||
jwt_valid_token=jwt_token,
|
||||
|
|
@ -2916,7 +2916,7 @@ async def test_find_and_validate_specific_team_id_hints_bracket_notation():
|
|||
# token has roles as a list — dot-notation won't find anything
|
||||
token = {"roles": ["team1"]}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="is not supported\\. Use 'roles' instead — LiteLLM") as exc_info:
|
||||
await JWTAuthManager.find_and_validate_specific_team_id(
|
||||
jwt_handler=handler,
|
||||
jwt_valid_token=token,
|
||||
|
|
@ -2947,7 +2947,7 @@ async def test_find_and_validate_specific_team_id_hints_bracket_index_notation()
|
|||
handler = _make_jwt_handler("roles[0]")
|
||||
token = {"roles": ["team1"]}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="is not supported in team_id_jwt_field\\. Use 'roles' instead") as exc_info:
|
||||
await JWTAuthManager.find_and_validate_specific_team_id(
|
||||
jwt_handler=handler,
|
||||
jwt_valid_token=token,
|
||||
|
|
@ -2977,7 +2977,7 @@ async def test_find_and_validate_specific_team_id_no_hint_for_valid_field():
|
|||
handler = _make_jwt_handler("appid")
|
||||
token = {} # no appid — triggers the "no team found" path
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match="No team found in token\\. Checked team_id field 'appid' and") as exc_info:
|
||||
await JWTAuthManager.find_and_validate_specific_team_id(
|
||||
jwt_handler=handler,
|
||||
jwt_valid_token=token,
|
||||
|
|
@ -4807,7 +4807,7 @@ async def test_multi_issuer_jwt_unknown_issuer_falls_back_to_global_jwks(monkeyp
|
|||
kid="issuer-key",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='Missing JWT Public Key URL from environment\\.') as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
|
||||
assert "Missing JWT Public Key URL from environment." in str(exc.value)
|
||||
|
|
@ -4838,7 +4838,7 @@ async def test_multi_issuer_jwt_rejects_wrong_audience(monkeypatch):
|
|||
kid="issuer-key",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match="Validation fails: Audience doesn't match") as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
|
||||
assert "Validation fails" in str(exc.value)
|
||||
|
|
@ -4881,7 +4881,7 @@ async def test_multi_issuer_jwt_same_kid_does_not_cross_issuer_keys(monkeypatch)
|
|||
kid=shared_kid,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='Validation fails: Signature verification failed') as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
|
||||
assert "Validation fails" in str(exc.value)
|
||||
|
|
@ -4936,7 +4936,7 @@ def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
|||
issuer = "https://issuer.example.com"
|
||||
jwks_url = f"{issuer}/keys"
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='must configure audience or set') as exc:
|
||||
LiteLLM_JWTAuth(
|
||||
issuers=[
|
||||
{
|
||||
|
|
@ -4953,7 +4953,7 @@ def test_multi_issuer_jwt_rejects_audience_with_disable_audience_validation():
|
|||
issuer = "https://issuer.example.com"
|
||||
jwks_url = f"{issuer}/keys"
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
with pytest.raises(Exception, match='cannot set audience and disable_audience_validation=True') as exc:
|
||||
LiteLLM_JWTAuth(
|
||||
issuers=[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ async def test_refuses_to_map_non_identity_fields(configure_proxy, privileged_fi
|
|||
configure_proxy(mappings={privileged_field: f"x-{privileged_field}"})
|
||||
request = _request_with_headers({f"x-{privileged_field}": "proxy_admin"})
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
with pytest.raises(ValueError, match='proxy auth refuses to map non-identity UserAPIKeyAuth') as exc:
|
||||
await handle_oauth2_proxy_request(request)
|
||||
assert privileged_field in str(exc.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def test_non_admin_config_update_route_rejected():
|
|||
request.query_params = {}
|
||||
|
||||
# Test that calling /config/update route raises HTTPException with 403 status
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
@ -134,7 +134,7 @@ def test_user_banner_update_rejected_for_non_admin():
|
|||
request = MagicMock(spec=Request)
|
||||
request.query_params = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
@ -1814,7 +1814,7 @@ def test_internal_user_blocked_from_global_spend_routes(route):
|
|||
request = MagicMock(spec=Request)
|
||||
request.query_params = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
@ -1843,7 +1843,7 @@ def test_internal_user_view_only_blocked_from_global_spend_routes(route):
|
|||
request = MagicMock(spec=Request)
|
||||
request.query_params = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value,
|
||||
|
|
@ -2046,7 +2046,7 @@ def test_internal_user_blocked_from_admin_viewer_logs_routes(route):
|
|||
if route not in INTERNAL_USER_BLOCKED_SUBSET:
|
||||
return
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
@ -2530,7 +2530,7 @@ def test_non_admin_non_team_admin_cannot_access_config_update_but_can_attempt_re
|
|||
)
|
||||
|
||||
# /config/update is still blocked
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
@ -3188,7 +3188,7 @@ def test_internal_user_blocked_from_search_tool_writes(route):
|
|||
request = MagicMock(spec=Request)
|
||||
request.query_params = {}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='Only proxy admin can be used to generate, delete, update') as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
|
|||
|
|
@ -5337,7 +5337,7 @@ async def test_random_non_sk_token_is_rejected(monkeypatch):
|
|||
patch("litellm.proxy.proxy_server.master_key", "sk-master"),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", MagicMock()),
|
||||
):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='LiteLLM Virtual Key expected\\.') as exc_info:
|
||||
await user_api_key_auth(
|
||||
request=mock_request,
|
||||
api_key="Bearer not-a-real-token",
|
||||
|
|
@ -5539,7 +5539,7 @@ async def test_real_jwt_still_requires_license_when_jwt_auth_enabled(monkeypatch
|
|||
patch("litellm.proxy.proxy_server.master_key", "sk-master"),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", None),
|
||||
):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(Exception, match='JWT Auth is an enterprise only feature\\. You must be a') as exc_info:
|
||||
await user_api_key_auth(
|
||||
request=mock_request,
|
||||
api_key=f"Bearer {jwt_token}",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class TestPollingErrorSurfacing:
|
|||
}
|
||||
|
||||
with patch("requests.get", return_value=mock_response) as mock_get, patch("time.sleep"):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Your litellm CLI is out of date and uses a login flow') as exc_info:
|
||||
_poll_for_ready_data("http://test/sso/cli/poll/sk-legacy")
|
||||
|
||||
assert mock_get.call_count == 1
|
||||
|
|
@ -151,7 +151,7 @@ class TestStartCliSsoFlowErrors:
|
|||
mock_response.status_code = 404
|
||||
|
||||
with patch("requests.post", return_value=mock_response):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Either --base-url is wrong, or the proxy is older than') as exc_info:
|
||||
_start_cli_sso_flow("https://old-proxy.example.com")
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
@ -167,7 +167,7 @@ class TestStartCliSsoFlowErrors:
|
|||
mock_response.json.return_value = {"detail": "Too many CLI login attempts. Try again later."}
|
||||
|
||||
with patch("requests.post", return_value=mock_response):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Too many CLI login attempts\\. Try again later\\.') as exc_info:
|
||||
_start_cli_sso_flow("https://test.example.com")
|
||||
|
||||
assert "HTTP 429" in str(exc_info.value)
|
||||
|
|
@ -183,7 +183,7 @@ class TestStartCliSsoFlowErrors:
|
|||
mock_response.text = "<html>Sign in to corporate VPN</html>"
|
||||
|
||||
with patch("requests.post", return_value=mock_response):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='A proxy, load balancer, or auth gateway in front of') as exc_info:
|
||||
_start_cli_sso_flow("https://test.example.com")
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
@ -197,7 +197,7 @@ class TestStartCliSsoFlowErrors:
|
|||
from litellm.proxy.client.cli.commands.auth import _start_cli_sso_flow
|
||||
|
||||
with patch("requests.post", side_effect=requests.ConnectionError("Connection refused")):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Connection refused\\. Check that the proxy is running') as exc_info:
|
||||
_start_cli_sso_flow("https://unreachable.example.com")
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -622,7 +622,7 @@ def test_fresh_api_key_never_hands_out_a_rotated_key_it_could_not_save():
|
|||
def save(_record):
|
||||
raise OSError("disk full")
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with pytest.raises(OSError, match="disk full"):
|
||||
_fresh(STORED, save, http, now=lambda: 999_950.0)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -472,14 +472,14 @@ def test_get_invalid_params():
|
|||
client = ModelsManagementClient(base_url="http://localhost:8000")
|
||||
|
||||
# Test with no parameters
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Exactly one of model_id or model_name must be provided') as exc_info:
|
||||
client.get()
|
||||
assert "Exactly one of model_id or model_name must be provided" in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
# Test with both parameters
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='Exactly one of model_id or model_name must be provided') as exc_info:
|
||||
client.get(model_id="123", model_name="gpt-4")
|
||||
assert "Exactly one of model_id or model_name must be provided" in str(
|
||||
exc_info.value
|
||||
|
|
|
|||
|
|
@ -586,7 +586,7 @@ def test_initialize_callbacks_on_proxy_rejects_class_valued_entry(probe_config_p
|
|||
silently never run the hook. Config load must fail instead."""
|
||||
entry = f"{_PROBE_MODULE_NAME}.FloorMaxTokens"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='litellm_settings\\.callbacks entry') as exc_info:
|
||||
_load_callbacks([entry], probe_config_path)
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
@ -609,7 +609,7 @@ def test_initialize_callbacks_on_proxy_rejects_non_dispatchable_values(
|
|||
):
|
||||
entry = f"{_PROBE_MODULE_NAME}.{attribute}"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='litellm_settings\\.callbacks entry') as exc_info:
|
||||
_load_callbacks([entry], probe_config_path)
|
||||
|
||||
message = str(exc_info.value)
|
||||
|
|
@ -621,7 +621,7 @@ def test_initialize_callbacks_on_proxy_rejects_non_dispatchable_values(
|
|||
def test_initialize_callbacks_on_proxy_rejects_class_valued_non_list_value(probe_config_path):
|
||||
entry = f"{_PROBE_MODULE_NAME}.FloorMaxTokens"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
with pytest.raises(ValueError, match='litellm_settings\\.callbacks entry') as exc_info:
|
||||
_load_callbacks(entry, probe_config_path)
|
||||
|
||||
assert entry in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -42,5 +42,5 @@ class TestSafeFilename:
|
|||
safe_filename("..")
|
||||
|
||||
def test_empty_rejected(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Empty or unsafe filename'):
|
||||
safe_filename("")
|
||||
|
|
|
|||
|
|
@ -130,16 +130,16 @@ def test_parse_budget_reset_time_unset_defaults_to_midnight():
|
|||
|
||||
|
||||
def test_parse_budget_reset_time_invalid_string_raises():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="hour 'HH:MM' or 'HH:MM:SS' string, e\\.g\\."):
|
||||
parse_budget_reset_time("25:00")
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Invalid budget_reset_time 'noon'; expected a"):
|
||||
parse_budget_reset_time("noon")
|
||||
|
||||
|
||||
def test_parse_budget_reset_time_non_string_raises():
|
||||
# Unquoted "12:00" in YAML parses to the int 720; it must fail loudly,
|
||||
# not silently fall back to midnight.
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="hour 'HH:MM' string, e\\.g\\."):
|
||||
parse_budget_reset_time(720)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -308,9 +308,9 @@ async def test_partition_maintenance_issues_nothing_when_the_budget_is_already_s
|
|||
|
||||
|
||||
def test_unsupported_interval_raises():
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Unsupported partition interval: year'):
|
||||
period_start(date(2026, 6, 1), "year")
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match='Unsupported partition interval: year'):
|
||||
next_period_start(date(2026, 6, 1), "year")
|
||||
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue