diff --git a/docs/my-website/docs/providers/miromind.md b/docs/my-website/docs/providers/miromind.md new file mode 100644 index 00000000000..c2741ca57d3 --- /dev/null +++ b/docs/my-website/docs/providers/miromind.md @@ -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` | + +
+
+ +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` diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index b5e5aa4ea28..4d3792a1bb5 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -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"] } } diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 400edcac889..a6ae91978c6 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -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" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 269e7daecc1..5cef8aba609 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -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", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 1d577213a1b..06aa17c54d5 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -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", diff --git a/tests/test_litellm/llms/miromind/__init__.py b/tests/test_litellm/llms/miromind/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/miromind/test_miromind.py b/tests/test_litellm/llms/miromind/test_miromind.py new file mode 100644 index 00000000000..704ad39eb40 --- /dev/null +++ b/tests/test_litellm/llms/miromind/test_miromind.py @@ -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", [])