Skip to content

test: migrate openai, openai_like and openrouter legacy tests to tests/unit - #42128

Merged
yuneng-berri merged 3 commits into
mainfrom
litellm_migrate_tests_p11
Sep 20, 2026
Merged

yuneng-berri merged 3 commits into
mainfrom
litellm_migrate_tests_p11

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • 20 legacy test files sat in tests/test_litellm, outside the mutation-proven unit harness

How it solves it:

  • Every test judged with a red then green source mutation, all 221 kept
  • 20 files git mv'd to the mirrored path under tests/unit, 4 orphan __init__.py removed
  • Empty __init__.py added to the 17 tests/unit directories this PR creates, so same-basename files cannot collide at collection

Counts: kept 221 / deleted 0 / rewritten 0 (239 node ids, 18 from parametrize)

moved 239, passed 239 in [REDACTED SECRET]/gh/BerriAI/litellm/2194947 (every node id from this PR is passed in that job's junit)

That job is red from 4 collection errors in files this PR does not touch: base_llm/batches/test_transformation.py, fastcrw/search/test_transformation.py, nvidia_riva/audio_transcription/test_handler.py and test_transformation.py collide by basename with anthropic/batches/ on main (no __init__.py), all landed on main between 07:50 and 08:09 UTC before this branch's first unit run. installing_litellm_on_python* and google_generate_content_endpoint_testing fail with the proxy master key boot check, which also fails on main (job 2194008)

Now uncovered: nothing, no test was deleted

Per-test ledger
llms/openai/embeddings/guardrail_translation/test_embeddings_guardrail_handler.py
test_embeddings_handler_string_input | keep unit | mutated: litellm/llms/openai/embeddings/guardrail_translation/handler.py:OpenAIEmbeddingsHandler._process_string_input: writes the whole guardrailed texts list into data["input"] instead of texts[0] | red then green
test_embeddings_handler_list_of_strings_input | keep unit | mutated: litellm/llms/openai/embeddings/guardrail_translation/handler.py:OpenAIEmbeddingsHandler._process_list_input: writes only guardrailed_texts[:1] into data["input"] | red then green
test_embeddings_guardrail_translation_mappings | keep unit | mutated: litellm/llms/openai/embeddings/guardrail_translation/__init__.py:guardrail_translation_mappings: drops the CallTypes.aembedding entry | red then green

llms/openai/image_generation/test_gpt_transformation.py
test_transform_image_generation_response_keeps_provider_echo | keep unit | mutated: litellm/llms/openai/image_generation/gpt_transformation.py:GPTImageGenerationConfig.transform_image_generation_response: overwrites the echoed quality with optional_params.get("quality", "high") instead of keeping the provider value | red then green

llms/openai/image_generation/test_image_generation_guardrail_handler.py
TestHandlerDiscovery::test_handler_discovered_for_image_generation | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/__init__.py:guardrail_translation_mappings: drops the CallTypes.image_generation entry | red then green
TestHandlerDiscovery::test_handler_discovered_for_aimage_generation | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/__init__.py:guardrail_translation_mappings: drops the CallTypes.aimage_generation entry | red then green
TestInputProcessing::test_process_simple_prompt | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/handler.py:OpenAIImageGenerationHandler.process_input_messages: writes the original prompt back instead of guardrailed_texts[0] | red then green
TestInputProcessing::test_process_no_prompt | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/handler.py:OpenAIImageGenerationHandler.process_input_messages: writes data["prompt"] = "" when no prompt is present | red then green
TestInputProcessing::test_process_empty_prompt | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/handler.py:OpenAIImageGenerationHandler.process_input_messages: writes data["prompt"] = "" when prompt is None | red then green
TestInputProcessing::test_process_complex_prompt | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/handler.py:OpenAIImageGenerationHandler.process_input_messages: writes the original prompt back instead of guardrailed_texts[0] | red then green
TestOutputProcessing::test_process_output_returns_unchanged | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/handler.py:OpenAIImageGenerationHandler.process_output_response: returns a fresh empty ImageResponse instead of the input | red then green
TestPIIMaskingScenario::test_pii_masking_in_prompt | keep unit | mutated: litellm/llms/openai/image_generation/guardrail_translation/handler.py:OpenAIImageGenerationHandler.process_input_messages: writes the original prompt back instead of guardrailed_texts[0] | red then green

llms/openai/image_generation/test_openai_image_generation_extra_headers.py
TestImageGenerationExtraHeaders::test_sync_image_generation_with_headers | keep unit | mutated: litellm/llms/openai/openai.py:OpenAIChatCompletion.image_generation: calls images.generate(**data) so caller headers never reach extra_headers | red then green
TestImageGenerationExtraHeaders::test_sync_image_generation_without_headers | keep unit | mutated: litellm/llms/openai/openai.py:OpenAIChatCompletion.image_generation: always passes extra_headers={} to images.generate | red then green
TestImageGenerationExtraHeaders::test_async_image_generation_with_headers | keep unit | mutated: litellm/llms/openai/openai.py:OpenAIChatCompletion.aimage_generation: calls images.generate(**data) so caller headers never reach extra_headers | red then green
TestImageGenerationExtraHeaders::test_async_image_generation_without_headers | keep unit | mutated: litellm/llms/openai/openai.py:OpenAIChatCompletion.aimage_generation: always passes extra_headers={} to images.generate | red then green
TestImageGenerationExtraHeaders::test_caller_headers_never_reach_the_logged_request_body | keep unit | mutated: litellm/llms/openai/openai.py:OpenAIChatCompletion.image_generation / aimage_generation: calls images.generate(**data) so caller headers never reach extra_headers (both parametrize cases) | red then green
TestImageGenerationExtraHeaders::test_sync_image_generation_forwards_headers_to_async | keep unit | mutated: litellm/llms/openai/openai.py:OpenAIChatCompletion.image_generation: forwards headers=None to aimage_generation when aimg_generation is True | red then green
TestImageGenerationEntryPointHeaders::test_extra_headers_reach_openai_provider | keep unit | mutated: litellm/images/main.py:image_generation: reads extra_headers as None so the caller's headers are dropped before reaching the provider | red then green

llms/openai/speech/test_text_to_speech_guardrail_handler.py
TestHandlerDiscovery::test_handler_discovered_for_speech | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/__init__.py:guardrail_translation_mappings: drops the CallTypes.speech entry | red then green
TestHandlerDiscovery::test_handler_discovered_for_aspeech | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/__init__.py:guardrail_translation_mappings: drops the CallTypes.aspeech entry | red then green
TestInputProcessing::test_process_simple_input_text | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestInputProcessing::test_process_no_input | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes data["input"] = "" when no input is present | red then green
TestInputProcessing::test_process_empty_input | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes data["input"] = "" when input is None | red then green
TestInputProcessing::test_process_long_input | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestInputProcessing::test_process_with_all_optional_params | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestOutputProcessing::test_output_processing_is_noop | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_output_response: returns None instead of the response | red then green
TestPIIMaskingScenario::test_pii_masking_in_tts_input | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestPIIMaskingScenario::test_phone_message_pii_redaction | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestContentModerationScenario::test_profanity_filtering_before_tts | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestMultilingualTTS::test_multilingual_input_preserved | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green
TestMultilingualTTS::test_all_voice_options | keep unit | mutated: litellm/llms/openai/speech/guardrail_translation/handler.py:OpenAITextToSpeechHandler.process_input_messages: writes the original input back instead of guardrailed_texts[0] | red then green

llms/openai/evals/test_openai_evals_transformation.py
test_validate_environment_sets_headers | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.validate_environment: sets Authorization to the bare api key without the "Bearer " prefix | red then green
test_validate_environment_requires_api_key | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.validate_environment: returns headers instead of raising ValueError when no api key is found | red then green
test_get_complete_url_with_eval_id | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.get_complete_url: returns f"{api_base}/v1/{endpoint}" even when eval_id is given | red then green
test_get_complete_url_encodes_eval_id | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.get_complete_url: interpolates the raw eval_id instead of encode_url_path_segment(eval_id) | red then green
test_get_complete_url_without_eval_id | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.get_complete_url: returns f"{api_base}/{endpoint}" dropping the /v1 prefix | red then green
test_transform_create_eval_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_create_eval_request: drops the testing_criteria key from the request body | red then green
test_transform_create_eval_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_create_eval_response: validates {**response_json, "name": ""} so the eval name is blanked | red then green
test_transform_list_evals_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_list_evals_request: never copies "after" into query_params | red then green
test_transform_list_evals_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_list_evals_response: forces has_more=True on the parsed response | red then green
test_transform_update_eval_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_update_eval_request: drops the metadata key from the request body | red then green
test_transform_delete_eval_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_delete_eval_request: builds the URL without eval_id (collection URL) | red then green
test_transform_delete_eval_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_delete_eval_response: forces deleted=False on the parsed response | red then green
test_transform_cancel_eval_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_cancel_eval_request: drops the trailing /cancel from the URL | red then green
test_transform_cancel_eval_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_cancel_eval_response: forces id="" on the parsed response | red then green
test_transform_run_requests_encode_eval_and_run_ids | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_cancel_run_request: interpolates the raw run_id instead of encoded_run_id | red then green
test_transform_get_eval_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_get_eval_response: validates {**response_json, "name": ""} so the eval name is blanked | red then green
test_transform_update_eval_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_update_eval_response: validates {**response_json, "name": ""} so the eval name is blanked | red then green
test_transform_create_run_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_create_run_response: forces status="failed" on the parsed run | red then green
test_transform_list_runs_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_list_runs_request: hardcodes query_params["order"] = "desc" | red then green
test_transform_list_runs_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_list_runs_response: forces has_more=True on the parsed response | red then green
test_transform_get_run_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_get_run_response: forces status="queued" on the parsed run | red then green
test_transform_cancel_run_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_cancel_run_response: forces status="queued" on the parsed run | red then green
test_transform_delete_run_request | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_delete_run_request: returns request_body {"run_id": run_id} instead of {} | red then green
test_transform_delete_run_response | keep unit | mutated: litellm/llms/openai/evals/transformation.py:OpenAIEvalsConfig.transform_delete_run_response: forces deleted=False on the parsed response | red then green

llms/openai/transcriptions/test_transcription_duration_hidden.py
TestDiarizedJsonUsageParsing::test_fractional_duration_seconds_does_not_raise | keep unit | mutated: litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py:convert_to_model_response_object: builds TranscriptionUsageDurationObject with seconds=int(usage["seconds"]) truncating 295.8 to 295 | red then green
TestDiarizedJsonUsageParsing::test_usage_duration_object_accepts_float_seconds | keep unit | mutated: litellm/types/utils.py:TranscriptionUsageDurationObject: seconds field typed int instead of float | red then green
TestTranscriptionDurationNotInResponseBody::test_convert_dict_stores_internal_duration_in_hidden_params | keep unit | mutated: litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py:convert_to_model_response_object: sets _audio_transcription_duration as a response attribute instead of _hidden_params["audio_transcription_duration"] | red then green
TestTranscriptionDurationNotInResponseBody::test_convert_dict_preserves_provider_duration | keep unit | mutated: litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py:convert_to_model_response_object: removes "duration" from the copied optional_keys | red then green
TestTranscriptionDurationNotInResponseBody::test_plain_json_response_has_no_duration | keep unit | mutated: litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py:convert_to_model_response_object: sets every optional key with response_object.get(key, 0.0) so duration becomes 0.0 | red then green
TestCostCalculatorReadsDurationFromHiddenParams::test_completion_cost_uses_hidden_params_duration | keep unit | mutated: litellm/cost_calculator.py:completion_cost: reads duration only from getattr(completion_response, "duration", 0.0), ignoring _hidden_params | red then green
TestCostCalculatorReadsDurationFromHiddenParams::test_completion_cost_falls_back_to_response_duration | keep unit | mutated: litellm/cost_calculator.py:completion_cost: falls back to 0.0 instead of completion_response.duration | red then green
TestCostCalculatorReadsDurationFromHiddenParams::test_completion_cost_defaults_to_zero_duration | keep unit | mutated: litellm/cost_calculator.py:completion_cost: default duration 1.0 instead of 0.0 when neither source has one | red then green

llms/openai/transcriptions/test_whisper_transformation.py
TestWhisperTransformRequestResponseFormat::test_defaults_to_verbose_json_when_unset | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_request: defaults response_format to "json" instead of "verbose_json" | red then green
TestWhisperTransformRequestResponseFormat::test_respects_explicit_json | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_request: unconditionally overwrites response_format with "verbose_json" | red then green
TestWhisperTransformRequestResponseFormat::test_respects_explicit_text | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_request: unconditionally overwrites response_format with "verbose_json" | red then green
TestWhisperTransformRequestResponseFormat::test_preserves_verbose_json_when_set | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_request: rewrites an explicit "verbose_json" to "json" | red then green
TestWhisperTransformResponse::test_parses_json_response | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_response: returns TranscriptionResponse(text="") for JSON bodies | red then green
TestWhisperTransformResponse::test_parses_plain_text_response | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_response: returns TranscriptionResponse(text="") for plain-text bodies | red then green
TestWhisperTransformResponse::test_malformed_json_body_with_json_content_type_raises | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_response: swallows JSONDecodeError for application/json bodies and returns the text as a transcription | red then green
TestWhisperTransformResponse::test_json_content_type_match_is_case_insensitive | keep unit | mutated: litellm/llms/openai/transcriptions/whisper_transformation.py:OpenAIWhisperAudioTranscriptionConfig.transform_audio_transcription_response: compares content-type without .lower() | red then green

llms/openai/transcriptions/test_audio_transcription_guardrail_handler.py
TestHandlerDiscovery::test_handler_discovered_for_transcription | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/__init__.py:guardrail_translation_mappings: dropped the CallTypes.transcription entry so lookup returns nothing | red then green
TestHandlerDiscovery::test_handler_discovered_for_atranscription | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/__init__.py:guardrail_translation_mappings: dropped the CallTypes.atranscription entry so lookup returns nothing | red then green
TestInputProcessing::test_input_processing_is_noop | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_input_messages: returns data with the "file" key stripped instead of the untouched dict | red then green
TestOutputProcessing::test_process_simple_transcription | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: writes original_text back instead of guardrailed_texts[0] | red then green
TestOutputProcessing::test_process_transcription_with_metadata | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: sets response.duration = None and response.language = None after applying the guardrailed text | red then green
TestOutputProcessing::test_process_empty_text | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: sets response.text = "" instead of returning the response untouched when text is None | red then green
TestOutputProcessing::test_process_long_transcription | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: writes original_text back instead of guardrailed_texts[0] | red then green
TestPIIMaskingScenario::test_pii_masking_in_transcription | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: writes original_text back instead of guardrailed_texts[0] | red then green
TestPIIMaskingScenario::test_meeting_transcription_pii_redaction | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: writes original_text back instead of guardrailed_texts[0] | red then green
TestContentModerationScenario::test_profanity_filtering | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: writes original_text back instead of guardrailed_texts[0] | red then green
TestMultilingualTranscription::test_transcription_preserves_language_info | keep unit | mutated: litellm/llms/openai/transcriptions/guardrail_translation/handler.py:OpenAIAudioTranscriptionHandler.process_output_response: sets response.language = None after applying the guardrailed text | red then green

llms/openai/vector_store_files/test_openai_vector_store_files_transformation.py
test_validate_environment_sets_headers | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.validate_environment: no longer adds the OpenAI-Beta assistants header | red then green
test_get_complete_url | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.get_complete_url: returns .../vector_stores/{id} without the trailing /files | red then green
test_get_complete_url_encodes_vector_store_id | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.get_complete_url: interpolates the raw vector_store_id instead of encode_url_path_segment(...) | red then green
test_transform_create_request | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.transform_create_vector_store_file_request: filtered_attributes = None so "attributes" is popped from the payload | red then green
test_transform_list_request | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.transform_list_vector_store_files_request: drops the "order" key from the returned query params | red then green
test_transform_file_request_encodes_file_id | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.transform_retrieve_vector_store_file_content_request: interpolates the raw file_id instead of encode_url_path_segment(...) | red then green
test_transform_create_response | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.transform_create_vector_store_file_response: returns {**response.json(), "status": "in_progress"} | red then green
test_transform_list_response | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.transform_list_vector_store_files_response: returns {**response.json(), "has_more": True} | red then green
test_transform_delete_response | keep unit | mutated: litellm/llms/openai/vector_store_files/transformation.py:OpenAIVectorStoreFilesConfig.transform_delete_vector_store_file_response: returns {**response.json(), "deleted": False} | red then green

llms/openai/vector_stores/test_openai_vector_stores_transformation.py
TestOpenAIVectorStoreAPIConfig::test_transform_create_vector_store_request_with_metadata_empty_or_none | keep unit | mutated: litellm/llms/openai/vector_stores/transformation.py:OpenAIVectorStoreConfig.transform_create_vector_store_request: metadata_payload = add_openai_metadata(metadata) or {"litellm": "true"} | red then green (both params)
TestOpenAIVectorStoreAPIConfig::test_transform_create_vector_store_request_with_large_metadata | keep unit | mutated: litellm/utils.py:add_openai_metadata: trims to 15 keys (idx < 15) instead of 16 | red then green
TestOpenAIVectorStoreAPIConfig::test_transform_search_vector_store_request_encodes_vector_store_id | keep unit | mutated: litellm/llms/openai/vector_stores/transformation.py:OpenAIVectorStoreConfig.transform_search_vector_store_request: interpolates the raw vector_store_id instead of encode_url_path_segment(...) | red then green
TestOpenAIVectorStoreAPIConfig::test_transform_search_vector_store_request_preserves_query_string | keep unit | mutated: litellm/llms/openai/vector_stores/transformation.py:OpenAIVectorStoreConfig.transform_search_vector_store_request: builds f"{base_url}/{id}/search" dropping the api_base query string | red then green

llms/openai/videos/test_openai_video_transformation.py
test_video_content_request_encodes_video_id_path_segment | keep unit | mutated: litellm/llms/openai/videos/transformation.py:OpenAIVideoConfig.transform_video_content_request: interpolates original_video_id unencoded into the /content URL | red then green
test_video_content_request_encodes_variant_query_param | keep unit | mutated: litellm/llms/openai/videos/transformation.py:OpenAIVideoConfig.transform_video_content_request: appends ?variant={variant} without quote(...) | red then green
test_wrapped_character_id_is_decoded_then_encoded_as_path_segment | keep unit | mutated: litellm/llms/openai/videos/transformation.py:OpenAIVideoConfig.transform_video_get_character_request: interpolates original_character_id unencoded into /characters/{id} | red then green
test_video_edit_request_forwards_uploaded_file_as_multipart | keep unit | mutated: litellm/llms/openai/videos/transformation.py:OpenAIVideoConfig.transform_video_edit_request: returns (url, form_data, None) dropping the multipart files tuple | red then green
test_video_edit_request_without_file_sends_json_id_reference | keep unit | mutated: litellm/llms/openai/videos/transformation.py:OpenAIVideoConfig.transform_video_edit_request: sends {"video_id": id} instead of {"video": {"id": id}} | red then green

llms/openai_like/chat/test_openai_like_chat_transformation.py
test_sanitize_usage_obj_handles_null_tokens | keep unit | mutated: litellm/llms/openai_like/chat/transformation.py:OpenAILikeChatConfig._sanitize_usage_obj: writes `usage[key] = 1` instead of `usage[key] = 0` for None *_tokens values | red then green
test_sanitize_usage_obj_no_usage | keep unit | mutated: litellm/llms/openai_like/chat/transformation.py:OpenAILikeChatConfig._sanitize_usage_obj: replaces the `if "usage" in response_json and isinstance(...)` guard with `if True:` so the sanitizer touches responses with no usage key (KeyError) | red then green
test_sanitize_usage_obj_valid_usage | keep unit | mutated: litellm/llms/openai_like/chat/transformation.py:OpenAILikeChatConfig._sanitize_usage_obj: adds `response_json["usage"] = {}` before return so the usage object is replaced with an empty dict (note: the test's shallow copy hides in-place mutations; rebinding the top-level key is what it detects) | red then green

llms/openai_like/embedding/test_openai_like_embedding.py
TestOpenAILikeEmbeddingHandler::test_encoding_format_none_filtered_out | keep unit | mutated: litellm/llms/openai_like/embedding/handler.py:OpenAILikeEmbeddingHandler.embedding: replaces the `v not in (None, "")` filter with `dict(optional_params)` so None values are sent in the request body | red then green
TestOpenAILikeEmbeddingHandler::test_encoding_format_empty_string_filtered_out | keep unit | mutated: litellm/llms/openai_like/embedding/handler.py:OpenAILikeEmbeddingHandler.embedding: same `dict(optional_params)` no-filter mutation sends "" through | red then green
TestOpenAILikeEmbeddingHandler::test_encoding_format_float_preserved | keep unit | mutated: litellm/llms/openai_like/embedding/handler.py:OpenAILikeEmbeddingHandler.embedding: appends `"encoding_format": "mutated"` into data after the filtered params spread, overwriting the caller value | red then green
TestOpenAILikeEmbeddingHandler::test_encoding_format_base64_preserved | keep unit | mutated: litellm/llms/openai_like/embedding/handler.py:OpenAILikeEmbeddingHandler.embedding: same overwrite mutation | red then green
TestOpenAILikeEmbeddingHandler::test_other_optional_params_preserved | keep unit | mutated: litellm/llms/openai_like/embedding/handler.py:OpenAILikeEmbeddingHandler.embedding: `dict(optional_params)` no-filter mutation leaks encoding_format=None into the body | red then green
TestOpenAILikeEmbeddingHandler::test_no_optional_params | keep unit | mutated: litellm/llms/openai_like/embedding/handler.py:OpenAILikeEmbeddingHandler.embedding: injected default `"encoding_format": "mutated"` appears in the body | red then green

llms/openai_like/messages/test_openai_like_anthropic_messages_transformation.py
test_get_complete_url_handles_api_base_variants | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.get_complete_url: drops `base.removesuffix("/v1")` so api_base ending in /v1 produces /v1/v1/messages | red then green
test_get_complete_url_requires_api_base | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.get_complete_url: returns "" instead of raising ValueError when api_base is missing | red then green
test_request_stays_in_anthropic_shape | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.transform_anthropic_messages_request: passes `model=f"{model}-mutated"` to the super() transform so payload["model"] changes | red then green
test_request_requires_max_tokens | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig.transform_anthropic_messages_request: defaults `max_tokens = 1024` instead of raising AnthropicError when max_tokens is missing | red then green
test_validate_environment_sets_bearer_and_anthropic_defaults | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py: DEFAULT_ANTHROPIC_API_VERSION changed "2023-06-01" -> "1999-01-01" | red then green
test_validate_environment_does_not_overwrite_caller_headers | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.validate_anthropic_messages_environment: `needs_auth` becomes `bool(api_key)` so a caller authorization header is overwritten | red then green
test_validate_environment_preserves_standard_cased_caller_headers | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.validate_anthropic_messages_environment: `present` computed as `set(headers)` instead of lowercased keys, so "Authorization" is not detected and a duplicate lowercase header is added | red then green
test_validate_environment_honors_x_api_key_when_present | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.validate_anthropic_messages_environment: `needs_auth = bool(api_key)` ignores x-api-key and still injects authorization | red then green
test_validate_environment_injects_anthropic_beta_for_context_management | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig._update_headers_with_anthropic_beta: `if has_other:` -> `if False:` so the context-management-2025-06-27 beta is never added | red then green
test_validate_environment_injects_anthropic_beta_for_fast_mode | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig._update_headers_with_anthropic_beta: speed check changed `== "fast"` to `== "slow"` so fast-mode-2026-02-01 is not added | red then green
test_validate_environment_merges_existing_anthropic_beta | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig._update_headers_with_anthropic_beta: `beta_values.update(existing_beta)` -> `beta_values.update(set())` so caller anthropic-beta values are dropped | red then green
test_request_strips_advisor_blocks_when_advisor_tool_absent | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig.transform_anthropic_messages_request: `if not _has_advisor:` -> `if False:` so advisor blocks are never stripped | red then green
test_request_maps_reasoning_effort_to_thinking | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic: `optional_params.pop("reasoning_effort", None)` -> `.get(...)` so reasoning_effort stays in the payload | red then green
test_passthrough_disables_anthropic_beta_filtering | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.should_filter_anthropic_beta_headers: returns True instead of False | red then green
test_anthropic_beta_survives_provider_filter_on_passthrough_path | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.should_filter_anthropic_beta_headers: returns True instead of False (the filtered beta values no longer survive) | red then green
test_json_provider_messages_config_probes_capabilities_under_provider_slug | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:JSONProviderAnthropicMessagesConfig.custom_llm_provider: returns "anthropic" instead of the provider slug | red then green
test_request_strips_cache_control_ttl_everywhere | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.supports_cache_control_ttl: returns True so normalize_cache_control_in_anthropic_payload is skipped and ttl survives | red then green
test_request_defaults_missing_cache_control_type_and_drops_non_dict | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.supports_cache_control_ttl: returns True so normalization (type defaulting / non-dict drop) is skipped | red then green
test_native_anthropic_config_keeps_cache_control_ttl | keep unit | mutated: litellm/llms/anthropic/experimental_pass_through/messages/transformation.py:AnthropicMessagesConfig.transform_anthropic_messages_request: applies normalize_cache_control_in_anthropic_payload on the native path so ttl is stripped | red then green
test_deployment_opt_in_keeps_cache_control_ttl | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:OpenAILikeAnthropicMessagesConfig.__init__: `self._cache_control_ttl = cache_control_ttl` -> `= False` so the opt-in is ignored | red then green
test_json_provider_constraint_opts_into_cache_control_ttl | keep unit | mutated: litellm/llms/openai_like/messages/transformation.py:JSONProviderAnthropicMessagesConfig.__init__: `cache_control_ttl=bool(provider.constraints.get("cache_control_ttl"))` -> `cache_control_ttl=False` | red then green
test_request_strips_ttl_only_where_the_messages_api_defines_cache_control | keep unit | mutated: litellm/llms/anthropic/common_utils.py:_with_portable_cache_control: blocks without their own cache_control now recurse into nested Mapping values so tool_use.input/input_schema cache_control data is rewritten | red then green

llms/openrouter/test_openrouter_embedding_transformation.py
test_openrouter_embedding_supported_params | keep unit | mutated: litellm/llms/openrouter/embedding/transformation.py:OpenrouterEmbeddingConfig.get_supported_openai_params: drops "dimensions" from the returned list | red then green
test_openrouter_embedding_transform_request | keep unit | mutated: litellm/llms/openrouter/embedding/transformation.py:OpenrouterEmbeddingConfig.transform_embedding_request: keeps `model = model` instead of stripping the "openrouter/" prefix | red then green
test_openrouter_embedding_validate_environment | keep unit | mutated: litellm/llms/openrouter/embedding/transformation.py:OpenrouterEmbeddingConfig.validate_environment: Content-Type header "application/json" -> "text/plain" | red then green
test_openrouter_embedding_get_complete_url | keep unit | mutated: litellm/llms/openrouter/embedding/transformation.py:OpenrouterEmbeddingConfig.get_complete_url: returns f"{api_base}/embeddingz" instead of f"{api_base}/embeddings" | red then green
test_openrouter_embedding_map_params | keep unit | mutated: litellm/llms/openrouter/embedding/transformation.py:OpenrouterEmbeddingConfig.map_openai_params: `param in supported` -> `param not in supported` so supported params are dropped and unsupported kept | red then green

llms/openrouter/test_openrouter_provider_routing.py
TestOpenRouterNativeModelRouting::test_double_prefixed_strips_once | keep unit | mutated: litellm/litellm_core_utils/get_llm_provider_logic.py:_get_openai_compatible_provider_info: `model.split("/", 1)[1]` -> `model.split("/")[-1]` so double-prefixed native ids are stripped twice | red then green
TestOpenRouterNativeModelRouting::test_bridge_double_call_preserves_native_model | keep unit | mutated: litellm/litellm_core_utils/get_llm_provider_logic.py:get_llm_provider openrouter block: swaps the two returns so a remainder without "/" returns `remainder` (strips "openrouter/" from the native id) | red then green
TestOpenRouterNativeModelRouting::test_regular_models_still_strip_normally | keep unit | mutated: litellm/litellm_core_utils/get_llm_provider_logic.py:_get_openai_compatible_provider_info: `model.split("/", 1)[1]` -> `model.split("/")[-1]` drops the vendor namespace | red then green
TestOpenRouterNativeModelRouting::test_wildcard_deployment_strips_routing_prefix | keep unit | mutated: litellm/litellm_core_utils/get_llm_provider_logic.py:get_llm_provider openrouter block: returns `model` instead of `remainder` so the routing prefix is kept | red then green

llms/openrouter/chat/test_openrouter_chat_transformation.py
TestOpenRouterChatCompletionStreamingHandler::test_chunk_parser_successful | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenRouterChatCompletionStreamingHandler.chunk_parser: sets `choice["delta"]["reasoning"] = "mutated"` so reasoning_content no longer matches the chunk's reasoning (setting reasoning_content=None stays green because the Delta model re-derives it from "reasoning") | red then green
TestOpenRouterChatCompletionStreamingHandler::test_chunk_parser_error_response | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenRouterChatCompletionStreamingHandler.chunk_parser: `status_code=error_message["code"]` -> `status_code=500` | red then green
TestOpenRouterChatCompletionStreamingHandler::test_chunk_parser_key_error | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenRouterChatCompletionStreamingHandler.chunk_parser: `except KeyError` -> `except TypeError` so KeyError propagates raw instead of OpenRouterException | red then green
test_openrouter_extra_body_transformation | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.transform_request: `response.update(extra_body)` -> `response.update({})` so provider routing keys are dropped | red then green
test_openrouter_cache_control_flag_removal | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.remove_cache_control_flag_from_messages_and_tools: non-supporting models return `messages, tools` unchanged instead of calling super() (the always-True _supports_cache_control_in_content mutation stays green because the flag is then moved into content) | red then green
test_openrouter_transform_request_with_cache_control | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig._move_cache_control_to_content: string-content branch writes `"cache_control": None` instead of the flag | red then green
test_openrouter_transform_request_with_cache_control_list_content | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig._move_cache_control_to_content: `if i == len(content) - 1` -> `if i == 0` so cache_control lands on the first block | red then green
test_openrouter_transform_request_with_cache_control_gemini | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig._move_cache_control_to_content: string-content branch writes `"cache_control": None` | red then green
test_openrouter_transform_request_multiple_cache_controls | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig._move_cache_control_to_content: first-block mutation (`i == 0`) | red then green
test_openrouter_cost_tracking_non_streaming | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.transform_request: `response["usage"] = {"include": True}` -> `{"include": False}` | red then green
test_openrouter_cost_tracking_streaming | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.transform_request: same usage include False mutation | red then green
test_openrouter_reasoning_models_allow_reasoning_effort_param | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.get_supported_openai_params: supports_reasoning check -> `if False:` so reasoning_effort is never appended | red then green
test_openrouter_non_reasoning_models_do_not_add_reasoning_effort | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.get_supported_openai_params: supports_reasoning check -> `if True:` so reasoning_effort is always appended | red then green
test_openrouter_reasoning_effort_max_maps_to_xhigh | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.map_openai_params: maps "max" to "high" instead of "xhigh" | red then green
test_openrouter_reasoning_effort_max_does_not_mutate_caller_dict | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.map_openai_params: `non_default_params["reasoning_effort"] = "xhigh"` in place instead of building a copied dict | red then green
test_openrouter_reasoning_effort_xhigh_passes_through | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.map_openai_params: adds `mapped_openai_params.pop("reasoning_effort", None)` so the param is dropped from the result | red then green
test_openrouter_reasoning_effort_high_passes_through | keep unit | mutated: litellm/llms/openrouter/chat/transformation.py:OpenrouterConfig.map_openai_params: same pop mutation | red then green

llms/openrouter/image_edit/test_openrouter_image_edit_transformation.py
TestOpenRouterImageEditTransformation::test_get_supported_openai_params | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:get_supported_openai_params: returns ["size", "quality"] dropping "n" | red then green
TestOpenRouterImageEditTransformation::test_use_multipart_form_data_returns_false | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:use_multipart_form_data: returns True instead of False | red then green
TestOpenRouterImageEditTransformation::test_map_openai_params_size | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:map_openai_params: aspect_ratio hardcoded to "9:16" instead of _map_size_to_aspect_ratio(value) | red then green
TestOpenRouterImageEditTransformation::test_map_openai_params_quality | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_quality_to_image_size: "high" maps to "2K" instead of "4K" | red then green
TestOpenRouterImageEditTransformation::test_map_openai_params_size_and_quality | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:map_openai_params: aspect_ratio hardcoded to "9:16" | red then green
TestOpenRouterImageEditTransformation::test_map_openai_params_n_passthrough | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:map_openai_params: else-branch `mapped_params[key] = value` -> `pass` so n is dropped | red then green
TestOpenRouterImageEditTransformation::test_map_openai_params_unknown_quality_ignored | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_quality_to_image_size: `.get(quality)` -> `.get(quality, "1K")` so unknown quality produces an image_config | red then green
TestOpenRouterImageEditTransformation::test_map_size_to_aspect_ratio_square | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_size_to_aspect_ratio: "512x512" -> "2:3" | red then green
TestOpenRouterImageEditTransformation::test_map_size_to_aspect_ratio_landscape | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_size_to_aspect_ratio: "1792x1024" -> "4:3" | red then green
TestOpenRouterImageEditTransformation::test_map_size_to_aspect_ratio_portrait | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_size_to_aspect_ratio: "1024x1536" -> "3:2" | red then green
TestOpenRouterImageEditTransformation::test_map_size_to_aspect_ratio_unknown_defaults_to_1_1 | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_size_to_aspect_ratio: `.get(size, "1:1")` -> `.get(size, "9:16")` | red then green
TestOpenRouterImageEditTransformation::test_map_quality_to_image_size | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_quality_to_image_size: "high" -> "2K" | red then green
TestOpenRouterImageEditTransformation::test_map_quality_to_image_size_unknown_returns_none | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_map_quality_to_image_size: `.get(quality, "1K")` default | red then green
TestOpenRouterImageEditTransformation::test_get_complete_url_default | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:get_complete_url: appends "/images/generations" instead of "/chat/completions" | red then green
TestOpenRouterImageEditTransformation::test_get_complete_url_with_custom_base | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:get_complete_url: same suffix mutation | red then green
TestOpenRouterImageEditTransformation::test_get_complete_url_with_complete_base | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:get_complete_url: `endswith("/chat/completions")` -> `endswith("/v1")` so the suffix is appended again | red then green
TestOpenRouterImageEditTransformation::test_validate_environment_with_api_key | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:validate_environment: `f"Bearer {api_key}"` -> `f"Token {api_key}"` | red then green
TestOpenRouterImageEditTransformation::test_validate_environment_with_secret_key | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:validate_environment: same Token prefix mutation | red then green
TestOpenRouterImageEditTransformation::test_validate_environment_missing_api_key_raises | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:validate_environment: `api_key = "fallback"` instead of raising ValueError | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_request_basic | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_request: `"modalities": ["image", "text"]` -> `["text"]` | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_request_with_bytesio | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_request: data URL `data:{mime};base64,{b64}` -> `data:{mime},{b64}` | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_request_with_multiple_images | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_request: `image if isinstance(image, list) else [image]` -> `[image]` so a list is treated as one image and _read_image_bytes raises | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_request_with_optional_params | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_request: `if key not in ("model","messages","modalities")` -> `if key in ()` so optional params are never copied | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_request_base64_encoding | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_request: `base64.b64encode(image_bytes).decode("utf-8")` reversed (`[::-1]`) | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_request_no_prompt | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_request: `if prompt:` -> `if True:` so a text part with None is added | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_response_with_base64 | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_response: `image_url.startswith("data:")` -> `startswith("https:")` so data URLs go down the url path | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_response_with_url | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_response: `url=image_url` -> `url=None` in the non-data branch | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_response_usage_and_cost | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_set_usage_and_cost: `output_tokens=image_tokens` -> `output_tokens=prompt_tokens` | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_response_multiple_images | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_response: `startswith("https:")` flip | red then green
TestOpenRouterImageEditTransformation::test_transform_image_edit_response_json_error | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:transform_image_edit_response: `except Exception` -> `except KeyError` so JSONDecodeError propagates raw | red then green
TestOpenRouterImageEditTransformation::test_get_error_class | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:get_error_class: `status_code=status_code` -> `status_code=500` | red then green
TestOpenRouterImageEditTransformation::test_read_image_bytes_from_bytes | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_read_image_bytes: bytes branch returns `image + b"pad"` | red then green
TestOpenRouterImageEditTransformation::test_read_image_bytes_from_bytesio | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_read_image_bytes: BytesIO branch drops the `image.seek(current_pos)` restore | red then green
TestOpenRouterImageEditTransformation::test_read_image_bytes_unsupported_type | keep unit | mutated: litellm/llms/openrouter/image_edit/transformation.py:_read_image_bytes: returns b"" instead of raising ValueError | red then green

llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py
TestOpenRouterImageGenerationTransformation::test_get_supported_openai_params | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:get_supported_openai_params: drops "n" from the list | red then green
TestOpenRouterImageGenerationTransformation::test_map_size_to_aspect_ratio_square | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_size_to_aspect_ratio: "512x512" -> "2:3" | red then green
TestOpenRouterImageGenerationTransformation::test_map_size_to_aspect_ratio_landscape | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_size_to_aspect_ratio: "1792x1024" -> "4:3" | red then green
TestOpenRouterImageGenerationTransformation::test_map_size_to_aspect_ratio_portrait | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_size_to_aspect_ratio: "1024x1536" -> "3:2" | red then green
TestOpenRouterImageGenerationTransformation::test_map_size_to_aspect_ratio_auto | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_size_to_aspect_ratio: "auto" -> "16:9" | red then green
TestOpenRouterImageGenerationTransformation::test_map_size_to_aspect_ratio_unknown | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_size_to_aspect_ratio: `.get(size, "1:1")` -> `.get(size, "9:16")` | red then green
TestOpenRouterImageGenerationTransformation::test_map_quality_to_image_size_low | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_quality_to_image_size: "low" -> "2K" | red then green
TestOpenRouterImageGenerationTransformation::test_map_quality_to_image_size_medium | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_quality_to_image_size: "medium" -> "1K" | red then green
TestOpenRouterImageGenerationTransformation::test_map_quality_to_image_size_high | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_quality_to_image_size: "high" -> "1K" | red then green
TestOpenRouterImageGenerationTransformation::test_map_quality_to_image_size_unknown | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_map_quality_to_image_size: `.get(quality, "1K")` default | red then green
TestOpenRouterImageGenerationTransformation::test_map_openai_params_size_only | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:map_openai_params: `if key in supported_params` -> `not in` so size goes through the passthrough branch without image_config | red then green
TestOpenRouterImageGenerationTransformation::test_map_openai_params_quality_only | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:map_openai_params: same inverted-supported mutation | red then green
TestOpenRouterImageGenerationTransformation::test_map_openai_params_size_and_quality | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:map_openai_params: same inverted-supported mutation | red then green
TestOpenRouterImageGenerationTransformation::test_map_openai_params_with_n_parameter | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:map_openai_params: same inverted-supported mutation | red then green
TestOpenRouterImageGenerationTransformation::test_map_openai_params_unsupported_param_drop_false | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:map_openai_params: same inverted-supported mutation | red then green
TestOpenRouterImageGenerationTransformation::test_map_openai_params_unsupported_param_drop_true | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:map_openai_params: same inverted-supported mutation | red then green
TestOpenRouterImageGenerationTransformation::test_get_complete_url_default | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:get_complete_url: default literal -> "https://openrouter.ai/api/v1/images/generations" | red then green
TestOpenRouterImageGenerationTransformation::test_get_complete_url_with_custom_base | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:get_complete_url: appends "/images/generations" instead of "/chat/completions" | red then green
TestOpenRouterImageGenerationTransformation::test_get_complete_url_with_base_already_complete | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:get_complete_url: `endswith("/chat/completions")` -> `endswith("/v1")` | red then green
TestOpenRouterImageGenerationTransformation::test_validate_environment_with_api_key | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:validate_environment: `f"Bearer {api_key}"` -> `f"Token {api_key}"` | red then green
TestOpenRouterImageGenerationTransformation::test_validate_environment_with_secret_key | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:validate_environment: same Token prefix mutation | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_request_basic | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_request: message role "user" -> "system" | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_request_with_image_config | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_request: `if key not in [...]` -> `if key in []` so image_config/n are dropped | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_response_with_base64_images | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_response: `image_url.startswith("data:")` -> `startswith("https:")` | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_response_with_url_images | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_response: `url=image_url` -> `url=None` | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_response_with_usage_and_cost | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:_set_usage_and_cost: `output_tokens=image_tokens` -> `output_tokens=total_tokens` | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_response_multiple_images | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_response: `startswith("https:")` flip | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_response_json_error | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_response: `except Exception` -> `except KeyError` | red then green
TestOpenRouterImageGenerationTransformation::test_transform_image_generation_response_transformation_error | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:transform_image_generation_response: `images = message.get("images", [])` -> `images = []` so no error is raised | red then green
TestOpenRouterImageGenerationTransformation::test_get_error_class | keep unit | mutated: litellm/llms/openrouter/image_generation/transformation.py:get_error_class: `status_code=status_code` -> `status_code=500` | red then green

User Flow

No user-facing behavior changes, test-only move

Relevant issues

Wave 2 phase 11 of the legacy test migration, follows #42103

Affected release

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Test-only move, proof is the CircleCI unit job junit linked above: 239 of 239 moved node ids passed

Link to Devin session: https://app.devin.ai/sessions/791d8c04e1e94bfe847e05193a265e16
Open in Devin Desktop: https://app.devin.ai/desktop/session/791d8c04e1e94bfe847e05193a265e16?variant=devin

…s/unit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team September 20, 2026 10:41
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge because it only relocates unchanged tests into a recursively collected unit-test directory

Summary

This PR relocates 20 unchanged OpenAI, OpenAI-like, and OpenRouter test files into the mutation-tested unit suite and removes four orphaned package initializers

  • All relocated files are exact renames with no test-content changes
  • The CircleCI unit job recursively discovers their new paths under tests/unit
  • No production behavior or security boundary changes

Reviews (1) · Last reviewed commit: "test: migrate openai, openai_like and op..."

@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

yuneng-berri and others added 2 commits September 20, 2026 11:52
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@yuneng-berri
yuneng-berri merged commit e5b2cab into main Sep 20, 2026
137 of 146 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_migrate_tests_p11 branch September 20, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant