Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/any_llm/providers/minimax/minimax.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ def _convert_completion_response_async(

async def chunk_iterator() -> AsyncIterator[ChatCompletionChunk]:
async for chunk in response:
if isinstance(chunk, OpenAIChatCompletionChunk):
if chunk.choices and chunk.choices[0].delta:
yield self._convert_completion_chunk_response(chunk)
if not isinstance(chunk, OpenAIChatCompletionChunk):
continue
if chunk.choices and chunk.choices[0].delta:
yield self._convert_completion_chunk_response(chunk)
elif chunk.usage is not None:
# Minimax can attach usage to a terminal chunk whose choice carries no delta,
# which fails chunk validation. Keep the usage and drop the unusable choices.
yield self._convert_completion_chunk_response(chunk.model_copy(update={"choices": []}))

return wrap_chunks_with_xml_reasoning(chunk_iterator())

Expand Down
107 changes: 106 additions & 1 deletion tests/unit/providers/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, cast

import pytest
from openai._models import construct_type
from openai.types.chat.chat_completion_chunk import (
ChatCompletionChunk as OpenAIChatCompletionChunk,
)
from openai.types.chat.chat_completion_chunk import (
Choice as OpenAIChunkChoice,
)
from openai.types.chat.chat_completion_chunk import (
ChoiceDelta as OpenAIChoiceDelta,
)
from openai.types.completion_usage import CompletionUsage
from pydantic import BaseModel

from any_llm import AnyLLM
from any_llm.exceptions import UnsupportedParameterError
from any_llm.providers.minimax.minimax import MinimaxProvider
from any_llm.types.completion import CompletionParams
from any_llm.types.completion import ChatCompletion, ChatCompletionChunk, CompletionParams

if TYPE_CHECKING:
from openai._streaming import AsyncStream


@pytest.fixture(autouse=True)
def _env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MINIMAX_API_KEY", "sk-minimax-test-123")


async def _iter_chunks(chunks: list[OpenAIChatCompletionChunk]) -> AsyncIterator[OpenAIChatCompletionChunk]:
for chunk in chunks:
yield chunk


def test_provider_basics() -> None:
"""Test provider instantiation and basic attributes."""
p = MinimaxProvider(api_key="sk-test")
Expand Down Expand Up @@ -73,3 +95,86 @@ def test_provider_metadata() -> None:
assert metadata.completion is True
assert metadata.embedding is False
assert metadata.image is False


@pytest.mark.asyncio
async def test_stream_preserves_usage_only_chunk() -> None:
"""Keep usage-only tail chunks while filtering unrelated empty chunks."""
provider = MinimaxProvider(api_key="sk-test")
chunks = [
OpenAIChatCompletionChunk(
id="minimax-content",
choices=[
OpenAIChunkChoice(
index=0,
finish_reason=None,
delta=OpenAIChoiceDelta(content="answer"),
)
],
created=1234567890,
model="MiniMax-M3",
object="chat.completion.chunk",
),
OpenAIChatCompletionChunk(
id="minimax-empty",
choices=[],
created=1234567890,
model="MiniMax-M3",
object="chat.completion.chunk",
),
OpenAIChatCompletionChunk(
id="minimax-usage",
choices=[],
created=1234567890,
model="MiniMax-M3",
object="chat.completion.chunk",
usage=CompletionUsage(prompt_tokens=11, completion_tokens=2, total_tokens=13),
),
]
stream = cast("AsyncStream[OpenAIChatCompletionChunk]", _iter_chunks(chunks))

converted = provider._convert_completion_response_async(stream)

assert not isinstance(converted, ChatCompletion)
result: list[ChatCompletionChunk] = [chunk async for chunk in converted]
assert len(result) == 2
assert result[0].choices[0].delta.content == "answer"
assert result[0].usage is None
assert result[1].choices == []
assert result[1].usage is not None
assert result[1].usage.prompt_tokens == 11
assert result[1].usage.completion_tokens == 2
assert result[1].usage.total_tokens == 13
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@pytest.mark.asyncio
async def test_stream_preserves_usage_on_chunk_without_delta() -> None:
"""Minimax attaches usage to a terminal chunk whose choice has no delta (see #657)."""
provider = MinimaxProvider(api_key="sk-test")
terminal = cast(
"OpenAIChatCompletionChunk",
construct_type(
value={
"id": "minimax-terminal",
"object": "chat.completion.chunk",
"created": 1234567890,
"model": "MiniMax-M3",
"choices": [
{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "answer"}}
],
"usage": {"prompt_tokens": 11, "completion_tokens": 2, "total_tokens": 13},
},
type_=OpenAIChatCompletionChunk,
),
)
assert terminal.choices[0].delta is None

stream = cast("AsyncStream[OpenAIChatCompletionChunk]", _iter_chunks([terminal]))
converted = provider._convert_completion_response_async(stream)

assert not isinstance(converted, ChatCompletion)
result: list[ChatCompletionChunk] = [chunk async for chunk in converted]
assert len(result) == 1
assert result[0].choices == []
assert result[0].usage is not None
assert result[0].usage.total_tokens == 13
Loading