feat(providers): add MiroMind deep research provider

MiroMind operates the MiroThinker deep research model family (agent
models that plan, search the web, and synthesize reports). The /v1/responses
endpoint is OpenAI-compatible, so this lands as a JSON-only provider
registration via openai_like/providers.json — no new Python module.

Two models registered:
  - miromind/mirothinker-1-7-deepresearch       (flagship, 256k context)
  - miromind/mirothinker-1-7-deepresearch-mini  (faster/smaller variant)

Both declare:
  - mode: "responses"             — routes litellm.responses() correctly
  - supports_native_streaming     — bypasses fake_stream (which would try
                                    to .json() the SSE body and fail)
  - supported_endpoints           — /v1/responses + /v1/chat/completions

Token pricing only — search-context pricing is intentionally omitted; see
docs/my-website/docs/providers/miromind.md for the rationale (MiroMind
bills page-fetch units, not search queries; the default web_search_call
counting in LiteLLM's cost tracker would overcount).

Tests: tests/test_litellm/llms/miromind/test_miromind.py (15 tests)
This commit is contained in:
callmemhz 2026-05-18 17:32:19 +08:00
parent 410ce761dc
commit ab251cd027
7 changed files with 321 additions and 0 deletions

View file

@ -0,0 +1,130 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# MiroMind
## Overview
| Property | Details |
|-------|-------|
| Description | MiroMind operates the [MiroThinker](https://miromind.ai) deep research model family — agent models that iteratively plan, search the web, fetch pages, and synthesize a final report. Exposed through an OpenAI-compatible Responses API. |
| Provider Route on LiteLLM | `miromind/` |
| Link to Provider Doc | [MiroMind API Documentation ↗](https://platform.miromind.ai/docs/responses-api) |
| Base URL | `https://api.miromind.ai/v1` |
| Supported Operations | [`/v1/responses`](#sample-usage), `/v1/chat/completions` |
<br />
<br />
MiroThinker models are **deep research agents** — they always run an internal planning loop with built-in `google_search` and URL-fetch tools. The Responses API is the recommended endpoint because it surfaces the full lifecycle (reasoning items, `web_search_call` events, final message) as typed SSE events. Chat Completions is supported as a thinner fallback that exposes only the final answer plus token usage.
## Available Models
| Model | Description | Context Window |
|-------|-------------|----------------|
| `miromind/mirothinker-1-7-deepresearch` | MiroThinker 1.7 flagship deep research agent | 262,144 tokens |
| `miromind/mirothinker-1-7-deepresearch-mini` | Smaller / faster variant | 262,144 tokens |
## Required Variables
```python showLineNumbers title="Environment Variables"
os.environ["MIROMIND_API_KEY"] = "" # your MiroMind API key
```
## Usage - LiteLLM Python SDK
### Streaming Responses (recommended)
```python showLineNumbers title="MiroMind Deep Research — streaming"
import os
import asyncio
import litellm
os.environ["MIROMIND_API_KEY"] = ""
async def main():
stream = await litellm.aresponses(
model="miromind/mirothinker-1-7-deepresearch-mini",
input="Find the latest news about LiteLLM and summarize in 3 bullets with sources.",
stream=True,
)
async for event in stream:
print(event.type, getattr(event, "delta", ""))
asyncio.run(main())
```
### Non-streaming Responses
```python showLineNumbers title="MiroMind Deep Research — non-streaming"
import os
import litellm
os.environ["MIROMIND_API_KEY"] = ""
resp = litellm.responses(
model="miromind/mirothinker-1-7-deepresearch-mini",
input="What were the most-cited papers on alignment in 2025?",
)
print(resp)
```
### Chat Completions (fallback)
```python showLineNumbers title="MiroMind via /v1/chat/completions"
import os
from litellm import completion
os.environ["MIROMIND_API_KEY"] = ""
response = completion(
model="miromind/mirothinker-1-7-deepresearch-mini",
messages=[{"role": "user", "content": "Summarize recent advances in retrieval-augmented generation."}],
)
print(response)
```
## Usage - LiteLLM Proxy Server
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: mirothinker
litellm_params:
model: miromind/mirothinker-1-7-deepresearch
api_key: os.environ/MIROMIND_API_KEY
- model_name: mirothinker-mini
litellm_params:
model: miromind/mirothinker-1-7-deepresearch-mini
api_key: os.environ/MIROMIND_API_KEY
```
## Custom API Base
```python showLineNumbers title="Custom API Base"
import os
os.environ["MIROMIND_API_BASE"] = "https://api.miromind.ai/v1"
os.environ["MIROMIND_API_KEY"] = ""
```
Or pass `api_base=` directly on the call.
## Responses API event shape
MiroThinker emits OpenAI Responses-shaped SSE events. Key things to know when integrating:
- **Reasoning** is emitted as raw chain-of-thought via `response.reasoning_text.delta` / `.done` (GPT-OSS-style), not `response.reasoning_summary_text.*`.
- **Built-in tools** surface as `web_search_call` output items with `action.type`:
- `google_search` → `action.type = "search"` (with `action.query`)
- URL fetch / page extraction → `action.type = "open_page"` (with `action.url`)
- **Custom events** (`response.agent_summary.*`) carry the model's planning-phase scratch text and can be safely ignored — clients listening only to `response.output_text.*` will see the final answer correctly.
See [MiroMind Responses API docs](https://platform.miromind.ai/docs/responses-api) for the full event catalog.
## Supported OpenAI Parameters
- `temperature`
- `top_p`
- `max_output_tokens`
- `stream`
- `tools` (built-in tools are managed server-side; client-supplied tool definitions are accepted but executed by MiroMind's agent loop)
- `instructions`

View file

@ -114,5 +114,11 @@
"param_mappings": {
"max_completion_tokens": "max_tokens"
}
},
"miromind": {
"base_url": "https://api.miromind.ai/v1",
"api_key_env": "MIROMIND_API_KEY",
"api_base_env": "MIROMIND_API_BASE",
"supported_endpoints": ["/v1/responses", "/v1/chat/completions"]
}
}

View file

@ -3325,6 +3325,7 @@ class LlmProviders(str, Enum):
A2A_AGENT = "a2a_agent"
LANGGRAPH = "langgraph"
MINIMAX = "minimax"
MIROMIND = "miromind"
SYNTHETIC = "synthetic"
APERTIS = "apertis"
NANOGPT = "nano-gpt"

View file

@ -23044,6 +23044,44 @@
"max_input_tokens": 200000,
"max_output_tokens": 8192
},
"miromind/mirothinker-1-7-deepresearch": {
"input_cost_per_token": 4e-06,
"output_cost_per_token": 2.5e-05,
"litellm_provider": "miromind",
"mode": "responses",
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"max_tokens": 16384,
"supported_endpoints": ["/v1/responses", "/v1/chat/completions"],
"search_context_cost_per_query": {
"search_context_size_low": 0.05,
"search_context_size_medium": 0.05,
"search_context_size_high": 0.05
},
"supports_native_streaming": true,
"supports_reasoning": true,
"supports_web_search": true,
"supports_system_messages": true
},
"miromind/mirothinker-1-7-deepresearch-mini": {
"input_cost_per_token": 1.25e-06,
"output_cost_per_token": 1e-05,
"litellm_provider": "miromind",
"mode": "responses",
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"max_tokens": 16384,
"supported_endpoints": ["/v1/responses", "/v1/chat/completions"],
"search_context_cost_per_query": {
"search_context_size_low": 0.05,
"search_context_size_medium": 0.05,
"search_context_size_high": 0.05
},
"supports_native_streaming": true,
"supports_reasoning": true,
"supports_web_search": true,
"supports_system_messages": true
},
"mistral.devstral-2-123b": {
"input_cost_per_token": 4e-07,
"litellm_provider": "bedrock_converse",

View file

@ -2491,6 +2491,22 @@
"responses": true
}
},
"miromind": {
"display_name": "MiroMind (`miromind`)",
"url": "https://docs.litellm.ai/docs/providers/miromind",
"endpoints": {
"chat_completions": true,
"messages": false,
"responses": true,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false
}
},
"pg_vector": {
"display_name": "PG Vector (`pg_vector`)",
"url": "https://docs.litellm.ai/docs/providers/pg_vector",

View file

@ -0,0 +1,130 @@
"""
Tests for the MiroMind JSON-driven provider.
MiroMind is registered via litellm/llms/openai_like/providers.json plus an
entry in the LlmProviders enum. There is no per-provider Python module —
everything routes through the OpenAI-like dynamic config machinery (see
tests/test_litellm/llms/openai_like/responses/test_openai_like_responses.py
for the generic mechanics). The tests here only assert that the miromind-
specific entries are wired into the registry and resolve to the documented
shape (responses endpoint, MiroMind base URL, `MIROMIND_API_KEY` env var).
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.abspath("../../.."))
class TestMiroMindProviderRegistration:
def test_provider_loaded_from_providers_json(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert JSONProviderRegistry.exists("miromind")
def test_provider_supports_responses_api(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert JSONProviderRegistry.supports_responses_api("miromind") is True
def test_provider_in_llm_providers_enum(self):
from litellm.types.utils import LlmProviders
assert LlmProviders("miromind") == LlmProviders.MIROMIND
class TestMiroMindResponsesConfig:
def _make_config(self):
from litellm.llms.openai_like.dynamic_config import (
create_responses_config_class,
)
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
provider = JSONProviderRegistry.get("miromind")
return create_responses_config_class(provider)()
def test_custom_llm_provider(self):
assert self._make_config().custom_llm_provider == "miromind"
def test_complete_url_default_base(self):
"""
With no api_base override and no MIROMIND_API_BASE env var, the
URL is built from providers.json `base_url`.
"""
from unittest.mock import patch
with patch(
"litellm.llms.openai_like.dynamic_config.get_secret_str",
return_value=None,
):
url = self._make_config().get_complete_url(
api_base=None, litellm_params={}
)
assert url == "https://api.miromind.ai/v1/responses"
def test_complete_url_api_base_override(self):
url = self._make_config().get_complete_url(
api_base="https://api-test.miromind.example/v1",
litellm_params={},
)
assert url == "https://api-test.miromind.example/v1/responses"
def test_validate_environment_sets_bearer_from_env(self):
from unittest.mock import patch
with patch(
"litellm.llms.openai_like.dynamic_config.get_secret_str",
return_value="sk-miromind-test",
):
headers = self._make_config().validate_environment(
headers={}, model="mirothinker-1-7-deepresearch-mini", litellm_params=None
)
assert headers["Authorization"] == "Bearer sk-miromind-test"
@pytest.mark.parametrize(
"model",
[
"miromind/mirothinker-1-7-deepresearch",
"miromind/mirothinker-1-7-deepresearch-mini",
],
)
class TestMiroMindModelRegistration:
"""Both flagship models must be declared in the source-of-truth
model_prices_and_context_window.json with mode=responses and
supports_native_streaming=true. We read the file directly (rather
than via litellm.model_cost) because the runtime fetches the cost
map from a remote URL at import time, which doesn't reflect the
in-PR edits until merge."""
@staticmethod
def _load_local_cost_map():
import json
import pathlib
# tests/test_litellm/llms/miromind/test_miromind.py → repo root → JSON
repo_root = pathlib.Path(__file__).resolve().parents[4]
path = repo_root / "model_prices_and_context_window.json"
with open(path, encoding="utf-8") as f:
return json.load(f)
def test_model_present(self, model):
assert model in self._load_local_cost_map()
def test_provider_and_mode(self, model):
entry = self._load_local_cost_map()[model]
assert entry["litellm_provider"] == "miromind"
assert entry["mode"] == "responses"
def test_supports_native_streaming(self, model):
"""Without this flag, LiteLLM falls back to fake_stream — which
tries to parse the SSE response as JSON and breaks every Responses
streaming call. See PR description for the failure mode."""
entry = self._load_local_cost_map()[model]
assert entry.get("supports_native_streaming") is True
def test_advertises_responses_endpoint(self, model):
entry = self._load_local_cost_map()[model]
assert "/v1/responses" in entry.get("supported_endpoints", [])