diff --git a/litellm/__init__.py b/litellm/__init__.py index d5a6af8173f..e36e94e10d1 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -644,7 +644,7 @@ snowflake_models: Set = set() gradient_ai_models: Set = set() llama_models: Set = set() nscale_models: Set = set() -apodex_models: Set = set() +apodex_models: Set = set() # mutable-ok: provider model registry is populated during initialization nebius_models: Set = set() nebius_embedding_models: Set = set() aiml_models: Set = set() diff --git a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py index 2a310fe4c1a..834d7437c43 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py @@ -74,7 +74,7 @@ class AnthropicResponsesStreamWrapper: if self._open_block_index is None: return self._chunk_queue.append( - { + { # mutable-ok: queued Anthropic event payload "type": "content_block_stop", "index": self._open_block_index, } @@ -88,7 +88,7 @@ class AnthropicResponsesStreamWrapper: { "type": "content_block_start", "index": block_idx, - "content_block": dict(content_block), + "content_block": dict(content_block), # mutable-ok: queued Anthropic event payload } ) self._open_block_index = block_idx @@ -143,7 +143,11 @@ class AnthropicResponsesStreamWrapper: block_idx = self._next_block_index() if item_id: self._item_id_to_block_index[item_id] = block_idx - self._start_block(block_idx, "text", {"type": "text", "text": ""}) + self._start_block( + block_idx, + "text", + {"type": "text", "text": ""}, # mutable-ok: Anthropic content block payload + ) elif item_type == "function_call": call_id: Final = ( getattr(item, "call_id", None) or (item.get("call_id") if isinstance(item, dict) else None) or "" @@ -172,7 +176,7 @@ class AnthropicResponsesStreamWrapper: text_block_idx: Final = self._get_or_start_block( item_id=item_id, block_type="text", - content_block={"type": "text", "text": ""}, + content_block={"type": "text", "text": ""}, # mutable-ok: Anthropic content block payload ) self._chunk_queue.append( { @@ -192,7 +196,11 @@ class AnthropicResponsesStreamWrapper: thinking_block_idx: Final = self._get_or_start_block( item_id=item_id, block_type="thinking", - content_block={"type": "thinking", "thinking": "", "signature": ""}, + content_block={ # mutable-ok: Anthropic content block payload + "type": "thinking", + "thinking": "", + "signature": "", + }, ) self._chunk_queue.append( { diff --git a/litellm/llms/apodex/chat/transformation.py b/litellm/llms/apodex/chat/transformation.py index 5b7e1e8ee6b..6287aedeebb 100644 --- a/litellm/llms/apodex/chat/transformation.py +++ b/litellm/llms/apodex/chat/transformation.py @@ -125,13 +125,15 @@ class ApodexChatConfig(OpenAIGPTConfig): def transform_request( self, model: str, - messages: list[AllMessageValues], + messages: list[AllMessageValues], # mutable-ok: matches the base-class signature optional_params: dict, # mutable-ok: matches the base-class signature litellm_params: dict, # mutable-ok: matches the base-class signature headers: dict, # mutable-ok: matches the base-class signature ) -> dict: # mutable-ok: JSON request body pin_non_streaming: Final = bool(optional_params.get(_PIN_NON_STREAMING, False)) - forwarded_params: Final = {key: value for key, value in optional_params.items() if key != _PIN_NON_STREAMING} + forwarded_params: Final = { # mutable-ok: base transformer requires a request dict + key: value for key, value in optional_params.items() if key != _PIN_NON_STREAMING + } transformed: Final = super().transform_request( model=model, messages=messages, diff --git a/litellm/llms/apodex/responses/transformation.py b/litellm/llms/apodex/responses/transformation.py index 1e6d2fbd1f4..86357e118be 100644 --- a/litellm/llms/apodex/responses/transformation.py +++ b/litellm/llms/apodex/responses/transformation.py @@ -117,13 +117,13 @@ class ApodexResponsesConfig(OpenAIResponsesAPIConfig): status_code=raw_response.status_code, # Content-Encoding and Content-Length describe the body being replaced here; # carrying them over makes httpx try to decompress plain JSON on read. - headers={ + headers={ # mutable-ok: httpx requires mutable response headers name: value for name, value in raw_response.headers.items() if name.lower() not in _BODY_FRAMING_HEADERS }, - json={ + json={ # mutable-ok: httpx requires a JSON-compatible response dict **payload, "created_at": payload.get("created_at", int(time())), - "output": payload.get("output", []), + "output": payload.get("output", []), # mutable-ok: Responses payload requires an array default }, ) return super().transform_cancel_response_api_response( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_streaming_iterator.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_streaming_iterator.py index d0da2056e84..28339143b72 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_streaming_iterator.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_streaming_iterator.py @@ -245,10 +245,16 @@ class TestProcessEventTextDeltaWithoutOutputItemAdded: assert chunks[1]["delta"] == {"type": "text_delta", "text": "Hel"} def test_process_event_delta_without_item_id_never_yields_negative_index(self): - chunks = _process_all([{"type": "response.output_text.delta", "delta": "Hi"}]) + chunks = _process_all( + [ + {"type": "response.output_text.delta", "delta": "Hi"}, + {"type": "response.output_text.delta", "delta": " again"}, + ] + ) assert [(c["type"], c["index"]) for c in chunks] == [ ("content_block_start", 0), ("content_block_delta", 0), + ("content_block_delta", 0), ] def test_process_event_unregistered_item_id_opens_new_text_block(self): diff --git a/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py b/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py index 7cfbefca047..75bb3068ca3 100644 --- a/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py +++ b/tests/test_litellm/llms/apodex/test_apodex_chat_transformation.py @@ -72,6 +72,13 @@ def _chat_config(model: str): class TestProviderResolution: + def test_openai_compatible_provider_info_uses_apodex_credentials(self): + config = _chat_config("apodex-1.1") + assert config._get_openai_compatible_provider_info("https://override.test/v1", "sk-override") == ( + "https://override.test/v1", + "sk-override", + ) + def test_prefixed_model_resolves_to_the_default_base(self): model, provider, api_key, api_base = litellm.get_llm_provider(model=CORE_MODEL) assert (model, provider, api_key, api_base) == ( diff --git a/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py b/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py index 0e4fb368cc5..ed6da35bba6 100644 --- a/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py +++ b/tests/test_litellm/llms/apodex/test_apodex_messages_transformation.py @@ -55,6 +55,7 @@ class TestNativePassthroughRouting: assert config is not None assert type(config).__name__ == "ApodexAnthropicMessagesConfig" assert config.custom_llm_provider == "apodex" + assert config.should_strip_billing_metadata() is True @pytest.mark.parametrize("model", DEEP_RESEARCH_MODELS) def test_deep_research_models_fall_back_to_translation(self, model: str): diff --git a/tests/test_litellm/llms/apodex/test_apodex_responses_transformation.py b/tests/test_litellm/llms/apodex/test_apodex_responses_transformation.py index 032fb99db67..252a07bf607 100644 --- a/tests/test_litellm/llms/apodex/test_apodex_responses_transformation.py +++ b/tests/test_litellm/llms/apodex/test_apodex_responses_transformation.py @@ -367,6 +367,18 @@ class TestDeepResearchKeepsState: assert event.type == "response.swarm.llm_delta" + def test_non_string_delta_is_not_claimed(self): + config = _responses_config("apodex-1-1-deep-research") + assert ( + config._map_swarm_delta( + { + "type": "response.swarm.llm_delta", + "swarm": {"data": {"channel": "output_text", "delta": None}}, + } + ) + is None + ) + @pytest.mark.parametrize( "event_type", (