mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
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:
parent
410ce761dc
commit
ab251cd027
7 changed files with 321 additions and 0 deletions
130
docs/my-website/docs/providers/miromind.md
Normal file
130
docs/my-website/docs/providers/miromind.md
Normal 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`
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
0
tests/test_litellm/llms/miromind/__init__.py
Normal file
0
tests/test_litellm/llms/miromind/__init__.py
Normal file
130
tests/test_litellm/llms/miromind/test_miromind.py
Normal file
130
tests/test_litellm/llms/miromind/test_miromind.py
Normal 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", [])
|
||||
Loading…
Add table
Reference in a new issue