Repository: BerriAI/litellm

Issue:
[Bug]: acompletion with streaming enabled for Groq LLMs is broken
### What happened?

Any acompletion call to a Groq LLM with streaming enabled fails with error: `litellm.exceptions.InternalServerError: litellm.InternalServerError: InternalServerError: GroqException - 'ascii' codec can't encode character '\xb5' in position 6: ordinal not in range(128)`

Taking sample from [docs](https://docs.litellm.ai/docs/completion/stream#usage-2)

```
from litellm import acompletion
import asyncio, os, traceback

async def completion_call():
    try:
        print("test acompletion + streaming")
        response = await acompletion(
            model="groq/llama-3.3-70b-versatile",
            messages=[{"content": "Hello, how are you?", "role": "user"}], 
            stream=True
        )
        print(f"response: {response}")
        async for chunk in response:
            print(chunk)
    except:
        print(f"error occurred: {traceback.format_exc()}")
        pass

asyncio.run(completion_call())
```

you'll get the error.


### Relevant log output

```shell
During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/some_path.venv/lib/python3.12/site-packages/litellm/main.py", line 543, in acompletion
    response = await init_response
               ^^^^^^^^^^^^^^^^^^^
  File "/Users/some_path.venv/lib/python3.12/site-packages/litellm/llms/custom_httpx/llm_http_handler.py", line 601, in acompletion_stream_function
    completion_stream, _response_headers = await self.make_async_call_stream_helper(
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/some_path.venv/lib/python3.12/site-packages/litellm/llms/custom_httpx/llm_http_handler.py", line 660, in make_async_call_stream_helper
    response = await self._make_common_async_call(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/some_path.venv/lib/python3.12/site-packages/litellm/llms/custom_httpx/llm_http_handler.py", line 138, in _make_common_async_call
    raise self._handle_error(e=e, provider_config=provider_config)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/some_path.venv/lib/python3.12/site-packages/litellm/llms/custom_httpx/llm_http_handler.py", line 2405, in _handle_error
    raise provider_config.get_error_class(
litellm.llms.openai.common_utils.OpenAIError: 'ascii' codec can't encode character '\xb5' in position 6: ordinal not in range(128)
```

### Are you a ML Ops Team?

No

### What LiteLLM version are you on ?

v1.70.2

### Twitter / LinkedIn details

_No response_


Discussion:
on `litellm==1.70` it works. starting on `litellm>=1.71` it breaks.
I just used this on the latest stable release

```from litellm import acompletion
import asyncio
import os
import traceback

# Optionally set your Groq API key here if needed
# os.environ["GROQ_API_KEY"] = "your-groq-api-key"
os.environ["GROQ_API_KEY"] = GROQ_API_KEY
def print_divider():
    print("\n" + "="*40 + "\n")

async def completion_call():
    try:
        print("test acompletion + streaming")
        response = await acompletion(
            model="groq/llama-3.3-70b-versatile",
            messages=[{"content": "Hello, how are you?", "role": "user"}],
            stream=True
        )
        print(f"response: {response}")
        async for chunk in response:
            print(chunk)
    except Exception:
        print(f"error occurred: {traceback.format_exc()}")
        pass

if __name__ == "__main__":
    print_divider()
    asyncio.run(completion_call())
    print_divider()

```

And it seems to be working fine for me 
Can you tell some steps to repro this issue ?

@jugaldb on `1.74.7`?
on `1.74.7` I still see:

```
  File "/Users/dev/llmtest/.venv/lib/python3.12/site-packages/litellm/main.py", line 562, in acompletion
    raise exception_type(
          ^^^^^^^^^^^^^^^
  File "/Users/dev/llmtest/.venv/lib/python3.12/site-packages/litellm/litellm_core_utils/exception_mapping_utils.py", line 2301, in exception_type
    raise e
  File "/Users/dev/llmtest/.venv/lib/python3.12/site-packages/litellm/litellm_core_utils/exception_mapping_utils.py", line 501, in exception_type
    raise InternalServerError(
litellm.exceptions.InternalServerError: litellm.InternalServerError: InternalServerError: GroqException - 'ascii' codec can't encode character '\xb5' in position 6: ordinal not in range(128)
```

using `uv`, `uv add litellm==1.74.7` and run your same script



Patch:
diff --git a/litellm/llms/openai_like/chat/handler.py b/litellm/llms/openai_like/chat/handler.py
index 821fc9b7f157..ae0a3cc35516 100644
--- a/litellm/llms/openai_like/chat/handler.py
+++ b/litellm/llms/openai_like/chat/handler.py
@@ -49,8 +49,15 @@ async def make_call(
         model_response = ModelResponse(**response.json())
         completion_stream = MockResponseIterator(model_response=model_response)
     else:
+        # Use aiter_text with explicit UTF-8 encoding to avoid ASCII encoding errors
+        async def utf8_aiter_lines():
+            async for line in response.aiter_text(encoding='utf-8'):
+                for line_part in line.splitlines(keepends=True):
+                    if line_part.strip():
+                        yield line_part.rstrip('\r\n')
+        
         completion_stream = ModelResponseIterator(
-            streaming_response=response.aiter_lines(), sync_stream=False
+            streaming_response=utf8_aiter_lines(), sync_stream=False
         )
     # LOGGING
     logging_obj.post_call(
@@ -93,8 +100,15 @@ def make_sync_call(
         model_response = ModelResponse(**response.json())
         completion_stream = MockResponseIterator(model_response=model_response)
     else:
+        # Use iter_text with explicit UTF-8 encoding to avoid ASCII encoding errors
+        def utf8_iter_lines():
+            for line in response.iter_text(encoding='utf-8'):
+                for line_part in line.splitlines(keepends=True):
+                    if line_part.strip():
+                        yield line_part.rstrip('\r\n')
+        
         completion_stream = ModelResponseIterator(
-            streaming_response=response.iter_lines(), sync_stream=True
+            streaming_response=utf8_iter_lines(), sync_stream=True
         )
 
     # LOGGING


Test patch:
diff --git a/scripts/test_groq_streaming_issue.py b/scripts/test_groq_streaming_issue.py
new file mode 100644
index 000000000000..0a996c0c2099
--- /dev/null
+++ b/scripts/test_groq_streaming_issue.py
@@ -0,0 +1,54 @@
+"""
+Test script to reproduce the Groq streaming ASCII encoding issue.
+
+This reproduces the issue described in #12660 where streaming responses
+containing non-ASCII characters like µ cause encoding errors.
+"""
+import asyncio
+import os
+import traceback
+from litellm import acompletion
+
+async def test_groq_streaming_with_special_chars():
+    """Test that reproduces the ASCII encoding issue with Groq streaming."""
+    try:
+        print("Testing acompletion + streaming with Groq...")
+        
+        # Test message that should trigger the µ character or similar non-ASCII content
+        test_messages = [
+            {"content": "What is the symbol for micro? Please include the µ symbol in your response.", "role": "user"}
+        ]
+        
+        # This should trigger the ASCII encoding error described in the issue
+        response = await acompletion(
+            model="groq/llama-3.3-70b-versatile",
+            messages=test_messages, 
+            stream=True
+        )
+        
+        print(f"Response type: {type(response)}")
+        
+        # Try to iterate through the stream
+        async for chunk in response:
+            print(f"Chunk: {chunk}")
+            
+        print("✅ Test completed successfully - no encoding errors!")
+        
+    except Exception as e:
+        print(f"❌ Error occurred: {e}")
+        print(f"Error type: {type(e)}")
+        print(f"Traceback:\n{traceback.format_exc()}")
+        return False
+        
+    return True
+
+if __name__ == "__main__":
+    # Note: This requires GROQ_API_KEY to be set
+    if not os.getenv("GROQ_API_KEY"):
+        print("⚠️  GROQ_API_KEY not set. Skipping test.")
+    else:
+        success = asyncio.run(test_groq_streaming_with_special_chars())
+        if success:
+            print("🎉 All tests passed!")
+        else:
+            print("💥 Test failed!")
\ No newline at end of file
diff --git a/tests/test_litellm/test_groq_streaming_encoding.py b/tests/test_litellm/test_groq_streaming_encoding.py
new file mode 100644
index 000000000000..cb1d69a4a10e
--- /dev/null
+++ b/tests/test_litellm/test_groq_streaming_encoding.py
@@ -0,0 +1,129 @@
+"""
+Test for Groq streaming ASCII encoding issue fix.
+
+This test verifies that the OpenAI-like handler correctly handles
+UTF-8 encoded content in streaming responses, specifically fixing
+the ASCII encoding error described in issue #12660.
+"""
+import pytest
+import asyncio
+from unittest.mock import Mock, AsyncMock
+from litellm.llms.openai_like.chat.handler import make_call, make_sync_call
+
+class MockResponse:
+    """Mock httpx response for testing UTF-8 handling."""
+    
+    def __init__(self, test_content: str):
+        self.test_content = test_content
+        self.status_code = 200
+        
+    def iter_text(self, encoding='utf-8'):
+        """Mock iter_text that yields content with the specified encoding."""
+        yield self.test_content
+        
+    async def aiter_text(self, encoding='utf-8'):
+        """Mock aiter_text that yields content with the specified encoding."""
+        yield self.test_content
+        
+    def json(self):
+        return {"choices": [{"delta": {"content": "test"}}]}
+
+class MockSyncClient:
+    """Mock synchronous HTTP client for testing."""
+    
+    def __init__(self, response_content: str):
+        self.response_content = response_content
+        
+    def post(self, *args, **kwargs):
+        return MockResponse(self.response_content)
+
+class MockAsyncClient:
+    """Mock asynchronous HTTP client for testing."""
+    
+    def __init__(self, response_content: str):
+        self.response_content = response_content
+        
+    async def post(self, *args, **kwargs):
+        return MockResponse(self.response_content)
+
+def test_utf8_streaming_sync():
+    """Test that synchronous streaming handles UTF-8 characters correctly."""
+    # Content with the µ character that was causing issues
+    test_content = "data: {\"choices\":[{\"delta\":{\"content\":\"The symbol µ represents micro\"}}]}\n\n"
+    
+    mock_client = MockSyncClient(test_content)
+    mock_logging = Mock()
+    
+    # This should not raise an ASCII encoding error
+    completion_stream = make_sync_call(
+        client=mock_client,
+        api_base="https://test.com/v1/chat/completions",
+        headers={"Authorization": "Bearer test"},
+        data='{"model": "test", "messages": []}',
+        model="test-model",
+        messages=[],
+        logging_obj=mock_logging
+    )
+    
+    # Verify we can iterate through the stream without encoding errors
+    assert completion_stream is not None
+
+@pytest.mark.asyncio
+async def test_utf8_streaming_async():
+    """Test that asynchronous streaming handles UTF-8 characters correctly."""
+    # Content with the µ character that was causing issues
+    test_content = "data: {\"choices\":[{\"delta\":{\"content\":\"The symbol µ represents micro\"}}]}\n\n"
+    
+    mock_client = MockAsyncClient(test_content)
+    mock_logging = Mock()
+    
+    # This should not raise an ASCII encoding error
+    completion_stream = await make_call(
+        client=mock_client,
+        api_base="https://test.com/v1/chat/completions",
+        headers={"Authorization": "Bearer test"},
+        data='{"model": "test", "messages": []}',
+        model="test-model",
+        messages=[],
+        logging_obj=mock_logging
+    )
+    
+    # Verify we can iterate through the stream without encoding errors
+    assert completion_stream is not None
+
+def test_various_unicode_characters():
+    """Test streaming with various Unicode characters that could cause issues."""
+    unicode_test_cases = [
+        "µ",  # Micro symbol (the original issue)
+        "©",  # Copyright symbol
+        "™",  # Trademark symbol
+        "€",  # Euro symbol
+        "北京", # Chinese characters
+        "🚀", # Emoji
+        "Ñoño", # Spanish characters with tildes
+    ]
+    
+    for unicode_char in unicode_test_cases:
+        test_content = f"data: {{\"choices\":[{{\"delta\":{{\"content\":\"Testing {unicode_char} character\"}}}}]}}\n\n"
+        
+        mock_client = MockSyncClient(test_content)
+        mock_logging = Mock()
+        
+        # This should not raise an ASCII encoding error for any Unicode character
+        completion_stream = make_sync_call(
+            client=mock_client,
+            api_base="https://test.com/v1/chat/completions",
+            headers={"Authorization": "Bearer test"},
+            data='{"model": "test", "messages": []}',
+            model="test-model",
+            messages=[],
+            logging_obj=mock_logging
+        )
+        
+        assert completion_stream is not None, f"Failed to handle Unicode character: {unicode_char}"
+
+if __name__ == "__main__":
+    test_utf8_streaming_sync()
+    asyncio.run(test_utf8_streaming_async())
+    test_various_unicode_characters()
+    print("All UTF-8 streaming tests passed!")
\ No newline at end of file