Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .semversioner/next-release/patch-20260713065517820226.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "patch",
"description": "Add MiniMax M3 and M2.7 model cost metadata."
}
25 changes: 25 additions & 0 deletions docs/config/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ embedding_models:

See [Detailed Configuration](yaml.md) for more details on configuration. [View LiteLLM basic usage](https://docs.litellm.ai/docs/#basic-usage) for details on how models are called (The `model_provider` is the portion prior to `/` while the `model` is the portion following the `/`).

## MiniMax Text Models

GraphRAG can call `MiniMax-M3` and `MiniMax-M2.7` through either supported API protocol. Use the `minimax` model provider for OpenAI-compatible requests and the `anthropic` model provider for Anthropic-compatible requests.

| Region | Protocol | `model_provider` | `api_base` |
| --- | --- | --- | --- |
| Global | OpenAI-compatible | `minimax` | `https://api.minimax.io/v1` |
| China | OpenAI-compatible | `minimax` | `https://api.minimaxi.com/v1` |
| Global | Anthropic-compatible | `anthropic` | `https://api.minimax.io/anthropic` |
| China | Anthropic-compatible | `anthropic` | `https://api.minimaxi.com/anthropic` |

Select the provider and base URL from the same row. LiteLLM appends `/chat/completions` to the OpenAI-compatible base or `/v1/messages` to the Anthropic-compatible base, so do not append `/v1` to an Anthropic-compatible base URL.

```yaml
completion_models:
default_completion_model:
model_provider: minimax
model: MiniMax-M3
auth_method: api_key
api_key: ${MINIMAX_API_KEY}
api_base: https://api.minimax.io/v1
```

Use `MiniMax-M2.7` as the model value when that model is required. The cost registry recognizes both protocol aliases, including long-context and service-tier pricing for `MiniMax-M3`.

## Model Selection Considerations

GraphRAG has been most thoroughly tested with the gpt-4 series of models from OpenAI, including gpt-4 gpt-4-turbo, gpt-4o, and gpt-4o-mini. Our [arXiv paper](https://arxiv.org/abs/2404.16130), for example, performed quality evaluation using gpt-4-turbo. As stated above, non-OpenAI models are supported through the use of LiteLLM but the suite of gpt-4 series of models from OpenAI remain the most tested and supported suite of models for GraphRAG – in other words, these are the models we know best and can help resolve issues with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,12 @@ def _process_lm_chat_completion(
response: "LLMCompletionResponse",
) -> None:
"""Process LMChatCompletion metrics."""
prompt_tokens = response.usage.prompt_tokens if response.usage else 0
completion_tokens = response.usage.completion_tokens if response.usage else 0
usage = response.usage
if usage is None:
return

prompt_tokens = usage.prompt_tokens
completion_tokens = usage.completion_tokens
total_tokens = prompt_tokens + completion_tokens

if total_tokens > 0:
Expand All @@ -89,13 +93,35 @@ def _process_lm_chat_completion(
metrics["total_tokens"] = total_tokens

model_id = f"{model_config.model_provider}/{model_config.model}"
model_costs = model_cost_registry.get_model_costs(model_id)
prompt_token_details = usage.prompt_tokens_details
cache_read_input_tokens = (
prompt_token_details.cached_tokens if prompt_token_details else 0
) or 0
usage_extra = usage.model_extra or {}
cache_read_input_tokens = int(
usage_extra.get("cache_read_input_tokens") or cache_read_input_tokens
)
cache_creation_input_tokens = int(
usage_extra.get("cache_creation_input_tokens") or 0
)
service_tier = (
response.service_tier
or input_args.get("service_tier")
or model_config.call_args.get("service_tier")
)
calculated_costs = model_cost_registry.calculate_costs(
model_id,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cache_read_input_tokens=cache_read_input_tokens,
cache_creation_input_tokens=cache_creation_input_tokens,
service_tier=service_tier,
)

if not model_costs:
if calculated_costs is None:
return

input_cost = prompt_tokens * model_costs["input_cost_per_token"]
output_cost = completion_tokens * model_costs["output_cost_per_token"]
input_cost, output_cost = calculated_costs
total_cost = input_cost + output_cost

metrics["responses_with_cost"] = 1
Expand All @@ -119,12 +145,16 @@ def _process_lm_embedding_response(
metrics["total_tokens"] = prompt_tokens

model_id = f"{model_config.model_provider}/{model_config.model}"
model_costs = model_cost_registry.get_model_costs(model_id)
calculated_costs = model_cost_registry.calculate_costs(
model_id,
prompt_tokens=prompt_tokens,
completion_tokens=0,
)

if not model_costs:
if calculated_costs is None:
return

input_cost = prompt_tokens * model_costs["input_cost_per_token"]
input_cost, _ = calculated_costs
metrics["responses_with_cost"] = 1
metrics["input_cost"] = input_cost
metrics["total_cost"] = input_cost
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,131 @@

"""Model cost registry module."""

from typing import Any, ClassVar, TypedDict
from typing import Any, ClassVar, TypedDict, cast

from litellm import model_cost
from typing_extensions import Self
from typing_extensions import Required, Self


class ModelCosts(TypedDict):
class ModelCosts(TypedDict, total=False):
"""Model costs."""

input_cost_per_token: float
output_cost_per_token: float
input_cost_per_token: Required[float]
output_cost_per_token: Required[float]
input_cost_per_token_above_512k_tokens: float
output_cost_per_token_above_512k_tokens: float
input_cost_per_token_priority: float
output_cost_per_token_priority: float
input_cost_per_token_above_512k_tokens_priority: float
output_cost_per_token_above_512k_tokens_priority: float
cache_read_input_token_cost: float
cache_read_input_token_cost_above_512k_tokens: float
cache_read_input_token_cost_priority: float
cache_read_input_token_cost_above_512k_tokens_priority: float
cache_creation_input_token_cost: float
max_input_tokens: int
max_output_tokens: int
litellm_provider: str
mode: str
source: str
supported_modalities: list[str]
supports_adaptive_thinking: bool
supports_function_calling: bool
supports_multimodal: bool
supports_prompt_caching: bool
supports_reasoning: bool
supports_system_messages: bool
supports_tool_choice: bool
supports_video_input: bool
supports_vision: bool


_MINIMAX_M3_COSTS: ModelCosts = {
"input_cost_per_token": 0.3 / 1_000_000,
"output_cost_per_token": 1.2 / 1_000_000,
"input_cost_per_token_above_512k_tokens": 0.6 / 1_000_000,
"output_cost_per_token_above_512k_tokens": 2.4 / 1_000_000,
"input_cost_per_token_priority": 0.45 / 1_000_000,
"output_cost_per_token_priority": 1.8 / 1_000_000,
"input_cost_per_token_above_512k_tokens_priority": 0.9 / 1_000_000,
"output_cost_per_token_above_512k_tokens_priority": 3.6 / 1_000_000,
"cache_read_input_token_cost": 0.06 / 1_000_000,
"cache_read_input_token_cost_above_512k_tokens": 0.12 / 1_000_000,
"cache_read_input_token_cost_priority": 0.09 / 1_000_000,
"cache_read_input_token_cost_above_512k_tokens_priority": 0.18 / 1_000_000,
"max_input_tokens": 1_000_000,
"max_output_tokens": 524_288,
"mode": "chat",
"source": "https://platform.minimax.io/docs/guides/pricing-paygo",
"supported_modalities": ["text", "image", "video"],
"supports_adaptive_thinking": True,
"supports_function_calling": True,
"supports_multimodal": True,
"supports_prompt_caching": True,
"supports_reasoning": True,
"supports_system_messages": True,
"supports_tool_choice": True,
"supports_video_input": True,
"supports_vision": True,
}

_MINIMAX_M27_COSTS: ModelCosts = {
"input_cost_per_token": 0.3 / 1_000_000,
"output_cost_per_token": 1.2 / 1_000_000,
"cache_read_input_token_cost": 0.06 / 1_000_000,
"cache_creation_input_token_cost": 0.375 / 1_000_000,
"max_input_tokens": 204_800,
"max_output_tokens": 204_800,
"mode": "chat",
"source": "https://platform.minimax.io/docs/guides/pricing-paygo",
"supported_modalities": ["text"],
"supports_function_calling": True,
"supports_prompt_caching": True,
"supports_reasoning": True,
"supports_system_messages": True,
"supports_tool_choice": True,
}

ADDITIONAL_MODEL_COSTS: dict[str, ModelCosts] = {
"minimax/MiniMax-M3": cast(
"ModelCosts", {**_MINIMAX_M3_COSTS, "litellm_provider": "minimax"}
),
"anthropic/MiniMax-M3": cast(
"ModelCosts", {**_MINIMAX_M3_COSTS, "litellm_provider": "anthropic"}
),
"minimax/MiniMax-M2.7": cast(
"ModelCosts", {**_MINIMAX_M27_COSTS, "litellm_provider": "minimax"}
),
"anthropic/MiniMax-M2.7": cast(
"ModelCosts", {**_MINIMAX_M27_COSTS, "litellm_provider": "anthropic"}
),
}


def _select_token_cost(
costs: ModelCosts,
key: str,
*,
prompt_tokens: int,
service_tier: str | None,
) -> float:
"""Select a token cost using the request size and service tier."""
cost_map: dict[str, Any] = dict(costs)
selected_key = key

threshold_key = f"{key}_above_512k_tokens"
if prompt_tokens > 512_000 and threshold_key in cost_map:
selected_key = threshold_key

priority_key = f"{selected_key}_priority"
if (
service_tier is not None
and service_tier.lower() == "priority"
and priority_key in cost_map
):
selected_key = priority_key

return float(cost_map.get(selected_key, cost_map.get(key, 0.0)))


class ModelCostRegistry:
Expand All @@ -30,7 +144,8 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self:

def __init__(self):
if not hasattr(self, "_initialized"):
self._model_costs = model_cost
self._model_costs = cast("dict[str, ModelCosts]", model_cost)
self._model_costs.update(ADDITIONAL_MODEL_COSTS)
self._initialized = True

def register_model_costs(self, model: str, costs: ModelCosts) -> None:
Expand Down Expand Up @@ -61,5 +176,73 @@ def get_model_costs(self, model: str) -> ModelCosts | None:
"""
return self._model_costs.get(model)

def calculate_costs(
self,
model: str,
*,
prompt_tokens: int,
completion_tokens: int,
cache_read_input_tokens: int = 0,
cache_creation_input_tokens: int = 0,
service_tier: str | None = None,
) -> tuple[float, float] | None:
"""Calculate input and output costs for one model response."""
costs = self.get_model_costs(model)
if costs is None:
return None

prompt_tokens = max(prompt_tokens, 0)
completion_tokens = max(completion_tokens, 0)
if model not in ADDITIONAL_MODEL_COSTS:
return (
prompt_tokens * costs["input_cost_per_token"],
completion_tokens * costs["output_cost_per_token"],
)

cache_read_input_tokens = min(max(cache_read_input_tokens, 0), prompt_tokens)
remaining_tokens = prompt_tokens - cache_read_input_tokens
cache_creation_input_tokens = min(
max(cache_creation_input_tokens, 0), remaining_tokens
)
uncached_input_tokens = remaining_tokens - cache_creation_input_tokens

input_rate = _select_token_cost(
costs,
"input_cost_per_token",
prompt_tokens=prompt_tokens,
service_tier=service_tier,
)
output_rate = _select_token_cost(
costs,
"output_cost_per_token",
prompt_tokens=prompt_tokens,
service_tier=service_tier,
)
cache_read_rate = _select_token_cost(
costs,
"cache_read_input_token_cost",
prompt_tokens=prompt_tokens,
service_tier=service_tier,
)
cache_creation_rate = _select_token_cost(
costs,
"cache_creation_input_token_cost",
prompt_tokens=prompt_tokens,
service_tier=service_tier,
)

if "cache_read_input_token_cost" not in costs:
cache_read_rate = input_rate
if "cache_creation_input_token_cost" not in costs:
cache_creation_rate = input_rate

input_cost = (
uncached_input_tokens * input_rate
+ cache_read_input_tokens * cache_read_rate
+ cache_creation_input_tokens * cache_creation_rate
)
output_cost = completion_tokens * output_rate
return input_cost, output_cost


model_cost_registry = ModelCostRegistry()
4 changes: 4 additions & 0 deletions tests/unit/model_cost_registry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright (c) 2026 Microsoft Corporation.
# Licensed under the MIT License

"""Model cost registry tests."""
Loading