From 7a23e499c883b1b81e62f22bface201e53c99eb1 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:36:41 +0000 Subject: [PATCH 1/2] feat: route MiniMax through the Anthropic-compatible endpoint and add test coverage --- .env.example | 17 +++++++++- README.md | 20 +++++++++++- server.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++--- tests.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 200 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index f61e441b..5b6c154c 100644 --- a/.env.example +++ b/.env.example @@ -2,14 +2,24 @@ ANTHROPIC_API_KEY="your-anthropic-api-key" # Needed if proxying *to* Anthropic OPENAI_API_KEY="sk-..." GEMINI_API_KEY="your-google-ai-studio-key" +MINIMAX_API_KEY="your-minimax-api-key" # Optional: Provider Preference and Model Mapping -# Controls which provider (google, openai, or anthropic) is preferred for mapping haiku/sonnet. +# Controls which provider (google, openai, anthropic, or minimax) is preferred for mapping haiku/sonnet. # Defaults to openai if not set. # Set to "anthropic" for "just an Anthropic proxy" mode (no remapping) PREFERRED_PROVIDER="openai" OPENAI_BASE_URL="https://api.openai.com/v1" +# Optional: MiniMax base URLs. +# OpenAI-compatible endpoint (global); set to https://api.minimaxi.com/v1 for the China region. +MINIMAX_BASE_URL="https://api.minimax.io/v1" +# Anthropic-compatible endpoint (global); set to https://api.minimaxi.com/anthropic for the China region. +MINIMAX_ANTHROPIC_BASE_URL="https://api.minimax.io/anthropic" +# Route MiniMax chat completions through the Anthropic-compatible endpoint by +# default; set to "openai" to use the OpenAI-compatible endpoint instead. +MINIMAX_API_STYLE="anthropic" + # Optional: Specify the exact models to map haiku/sonnet to. # If PREFERRED_PROVIDER=google, these MUST be valid Gemini model names known to the server. # Defaults to gemini-2.5-pro and gemini-2.5-flash if PREFERRED_PROVIDER=google. @@ -23,6 +33,11 @@ OPENAI_BASE_URL="https://api.openai.com/v1" # BIG_MODEL="gemini-2.5-pro" # SMALL_MODEL="gemini-2.5-flash" +# Example MiniMax mapping: +# PREFERRED_PROVIDER="minimax" +# BIG_MODEL="MiniMax-M3" +# SMALL_MODEL="MiniMax-M2.7" + # Example Google with vertex AI auth via ADC: # PREFERRED_PROVIDER="google" # USE_VERTEX_AUTH=true diff --git a/README.md b/README.md index d9ae04c8..a4972432 100644 --- a/README.md +++ b/README.md @@ -42,17 +42,22 @@ A proxy server that lets you use Anthropic clients with Gemini, OpenAI, or Anthr * `ANTHROPIC_API_KEY`: (Optional) Needed only if proxying *to* Anthropic models. * `OPENAI_API_KEY`: Your OpenAI API key (Required if using the default OpenAI preference or as fallback). * `GEMINI_API_KEY`: Your Google AI Studio (Gemini) API key (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`). + * `MINIMAX_API_KEY`: Your MiniMax API key (Required if `PREFERRED_PROVIDER=minimax`). * `USE_VERTEX_AUTH` (Optional): Set to `true` to use Application Default Credentials (ADC) will be used (no static API key required). Note: when USE_VERTEX_AUTH=true, you must configure `VERTEX_PROJECT` and `VERTEX_LOCATION`. * `VERTEX_PROJECT` (Optional): Your Google Cloud Project ID (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`). * `VERTEX_LOCATION` (Optional): The Google Cloud region for Vertex AI (e.g., `us-central1`) (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`). - * `PREFERRED_PROVIDER` (Optional): Set to `openai` (default), `google`, or `anthropic`. This determines the primary backend for mapping `haiku`/`sonnet`. + * `PREFERRED_PROVIDER` (Optional): Set to `openai` (default), `google`, `anthropic`, or `minimax`. This determines the primary backend for mapping `haiku`/`sonnet`. * `BIG_MODEL` (Optional): The model to map `sonnet` requests to. Defaults to `gpt-4.1` (if `PREFERRED_PROVIDER=openai`) or `gemini-2.5-pro-preview-03-25`. Ignored when `PREFERRED_PROVIDER=anthropic`. * `SMALL_MODEL` (Optional): The model to map `haiku` requests to. Defaults to `gpt-4.1-mini` (if `PREFERRED_PROVIDER=openai`) or `gemini-2.0-flash`. Ignored when `PREFERRED_PROVIDER=anthropic`. + * `MINIMAX_BASE_URL` (Optional): The MiniMax OpenAI-compatible endpoint. Defaults to `https://api.minimax.io/v1` (global); set to `https://api.minimaxi.com/v1` for the China region. + * `MINIMAX_ANTHROPIC_BASE_URL` (Optional): The MiniMax Anthropic-compatible endpoint. Defaults to `https://api.minimax.io/anthropic` (global); set to `https://api.minimaxi.com/anthropic` for the China region. + * `MINIMAX_API_STYLE` (Optional): Which MiniMax endpoint to route chat completions through. Defaults to `anthropic` (the Anthropic-compatible endpoint); set to `openai` to use the OpenAI-compatible endpoint. **Mapping Logic:** - If `PREFERRED_PROVIDER=openai` (default), `haiku`/`sonnet` map to `SMALL_MODEL`/`BIG_MODEL` prefixed with `openai/`. - If `PREFERRED_PROVIDER=google`, `haiku`/`sonnet` map to `SMALL_MODEL`/`BIG_MODEL` prefixed with `gemini/` *if* those models are in the server's known `GEMINI_MODELS` list (otherwise falls back to OpenAI mapping). - If `PREFERRED_PROVIDER=anthropic`, `haiku`/`sonnet` requests are passed directly to Anthropic with the `anthropic/` prefix without remapping to different models. + - If `PREFERRED_PROVIDER=minimax`, `haiku`/`sonnet` map to `SMALL_MODEL`/`BIG_MODEL` prefixed with `minimax/`, and bare MiniMax models in the server's known `MINIMAX_MODELS` list are prefixed with `minimax/`. MiniMax requests use the MiniMax API key and are routed through the configured endpoint (the Anthropic-compatible one by default). 4. **Run the server**: ```bash @@ -185,6 +190,19 @@ PREFERRED_PROVIDER="anthropic" *Use case: This mode enables you to use the proxy infrastructure (for logging, middleware, request/response processing, etc.) while still using actual Anthropic models rather than being forced to remap to OpenAI or Gemini.* +**Example 3b: Prefer MiniMax** +```dotenv +MINIMAX_API_KEY="your-minimax-key" +PREFERRED_PROVIDER="minimax" +BIG_MODEL="MiniMax-M3" +SMALL_MODEL="MiniMax-M2.7" +# MINIMAX_ANTHROPIC_BASE_URL="https://api.minimax.io/anthropic" # Optional, it's the default (global) +# MINIMAX_BASE_URL="https://api.minimax.io/v1" # Optional, the OpenAI-compatible endpoint +# MINIMAX_API_STYLE="anthropic" # Optional, it's the default; set to "openai" for the OpenAI-compatible endpoint +``` + +*Use case: MiniMax requests are routed through the MiniMax Anthropic-compatible endpoint by default, so Anthropic clients can talk to MiniMax models without any format translation.* + **Example 4: Use Specific OpenAI Models** ```dotenv OPENAI_API_KEY="your-openai-key" diff --git a/server.py b/server.py index 15dacf35..defd6928 100644 --- a/server.py +++ b/server.py @@ -89,6 +89,7 @@ def format(self, record): ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") +MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY") # Get Vertex AI project and location from environment (if set) VERTEX_PROJECT = os.environ.get("VERTEX_PROJECT", "unset") @@ -100,6 +101,17 @@ def format(self, record): # Get OpenAI base URL from environment (if set) OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL") +# Get MiniMax base URLs from environment (if set) +# OpenAI-compatible endpoint (global); use https://api.minimaxi.com/v1 for the China region. +MINIMAX_BASE_URL = os.environ.get("MINIMAX_BASE_URL", "https://api.minimax.io/v1") +# Anthropic-compatible endpoint (global); use https://api.minimaxi.com/anthropic for the China region. +MINIMAX_ANTHROPIC_BASE_URL = os.environ.get( + "MINIMAX_ANTHROPIC_BASE_URL", "https://api.minimax.io/anthropic" +) +# Route MiniMax chat completions through the Anthropic-compatible endpoint by +# default; set to "openai" to use the OpenAI-compatible endpoint instead. +MINIMAX_API_STYLE = os.environ.get("MINIMAX_API_STYLE", "anthropic").lower() + # Get preferred provider (default to openai) PREFERRED_PROVIDER = os.environ.get("PREFERRED_PROVIDER", "openai").lower() @@ -127,6 +139,9 @@ def format(self, record): # List of Gemini models GEMINI_MODELS = ["gemini-2.5-flash", "gemini-2.5-pro"] +# List of MiniMax models +MINIMAX_MODELS = ["MiniMax-M3", "MiniMax-M2.7"] + # Helper function to clean schema for Gemini def clean_gemini_schema(schema: Any) -> Any: @@ -243,6 +258,8 @@ def validate_model_field(cls, v, info): # Renamed to avoid conflict clean_v = clean_v[7:] elif clean_v.startswith("gemini/"): clean_v = clean_v[7:] + elif clean_v.startswith("minimax/"): + clean_v = clean_v[8:] # --- Mapping Logic --- START --- mapped = False @@ -256,6 +273,9 @@ def validate_model_field(cls, v, info): # Renamed to avoid conflict if PREFERRED_PROVIDER == "google" and SMALL_MODEL in GEMINI_MODELS: new_model = f"gemini/{SMALL_MODEL}" mapped = True + elif PREFERRED_PROVIDER == "minimax": + new_model = f"minimax/{SMALL_MODEL}" + mapped = True else: new_model = f"openai/{SMALL_MODEL}" mapped = True @@ -265,6 +285,9 @@ def validate_model_field(cls, v, info): # Renamed to avoid conflict if PREFERRED_PROVIDER == "google" and BIG_MODEL in GEMINI_MODELS: new_model = f"gemini/{BIG_MODEL}" mapped = True + elif PREFERRED_PROVIDER == "minimax": + new_model = f"minimax/{BIG_MODEL}" + mapped = True else: new_model = f"openai/{BIG_MODEL}" mapped = True @@ -274,6 +297,9 @@ def validate_model_field(cls, v, info): # Renamed to avoid conflict if clean_v in GEMINI_MODELS and not v.startswith("gemini/"): new_model = f"gemini/{clean_v}" mapped = True # Technically mapped to add prefix + elif clean_v in MINIMAX_MODELS and not v.startswith("minimax/"): + new_model = f"minimax/{clean_v}" + mapped = True # Technically mapped to add prefix elif clean_v in OPENAI_MODELS and not v.startswith("openai/"): new_model = f"openai/{clean_v}" mapped = True # Technically mapped to add prefix @@ -283,7 +309,7 @@ def validate_model_field(cls, v, info): # Renamed to avoid conflict logger.debug(f"πŸ“Œ MODEL MAPPING: '{original_model}' ➑️ '{new_model}'") else: # If no mapping occurred and no prefix exists, log warning or decide default - if not v.startswith(("openai/", "gemini/", "anthropic/")): + if not v.startswith(("openai/", "gemini/", "anthropic/", "minimax/")): logger.warning( f"⚠️ No prefix or mapping rule for model: '{original_model}'. Using as is." ) @@ -326,6 +352,8 @@ def validate_model_token_count(cls, v, info): # Renamed to avoid conflict clean_v = clean_v[7:] elif clean_v.startswith("gemini/"): clean_v = clean_v[7:] + elif clean_v.startswith("minimax/"): + clean_v = clean_v[8:] # --- Mapping Logic --- START --- mapped = False @@ -334,6 +362,9 @@ def validate_model_token_count(cls, v, info): # Renamed to avoid conflict if PREFERRED_PROVIDER == "google" and SMALL_MODEL in GEMINI_MODELS: new_model = f"gemini/{SMALL_MODEL}" mapped = True + elif PREFERRED_PROVIDER == "minimax": + new_model = f"minimax/{SMALL_MODEL}" + mapped = True else: new_model = f"openai/{SMALL_MODEL}" mapped = True @@ -343,6 +374,9 @@ def validate_model_token_count(cls, v, info): # Renamed to avoid conflict if PREFERRED_PROVIDER == "google" and BIG_MODEL in GEMINI_MODELS: new_model = f"gemini/{BIG_MODEL}" mapped = True + elif PREFERRED_PROVIDER == "minimax": + new_model = f"minimax/{BIG_MODEL}" + mapped = True else: new_model = f"openai/{BIG_MODEL}" mapped = True @@ -352,6 +386,9 @@ def validate_model_token_count(cls, v, info): # Renamed to avoid conflict if clean_v in GEMINI_MODELS and not v.startswith("gemini/"): new_model = f"gemini/{clean_v}" mapped = True # Technically mapped to add prefix + elif clean_v in MINIMAX_MODELS and not v.startswith("minimax/"): + new_model = f"minimax/{clean_v}" + mapped = True # Technically mapped to add prefix elif clean_v in OPENAI_MODELS and not v.startswith("openai/"): new_model = f"openai/{clean_v}" mapped = True # Technically mapped to add prefix @@ -360,7 +397,7 @@ def validate_model_token_count(cls, v, info): # Renamed to avoid conflict if mapped: logger.debug(f"πŸ“Œ TOKEN COUNT MAPPING: '{original_model}' ➑️ '{new_model}'") else: - if not v.startswith(("openai/", "gemini/", "anthropic/")): + if not v.startswith(("openai/", "gemini/", "anthropic/", "minimax/")): logger.warning( f"⚠️ No prefix or mapping rule for token count model: '{original_model}'. Using as is." ) @@ -641,17 +678,34 @@ def convert_anthropic_to_litellm(anthropic_request: MessagesRequest) -> Dict[str f"Capping max_tokens to 16384 for OpenAI/Gemini model (original value: {anthropic_request.max_tokens})" ) + # MiniMax exposes both an Anthropic-compatible and an OpenAI-compatible + # endpoint. Route the model through the matching LiteLLM provider so the + # request format and api_base are selected consistently downstream. + litellm_model = anthropic_request.model + if litellm_model.startswith("minimax/"): + model_suffix = litellm_model[len("minimax/") :] + if MINIMAX_API_STYLE == "openai": + litellm_model = f"openai/{model_suffix}" + else: + litellm_model = f"anthropic/{model_suffix}" + # Create LiteLLM request dict litellm_request = { - "model": anthropic_request.model, # it understands "anthropic/claude-x" format + "model": litellm_model, # it understands "anthropic/claude-x" format "messages": messages, "max_completion_tokens": max_tokens, "temperature": anthropic_request.temperature, "stream": anthropic_request.stream, } - # Only include thinking field for Anthropic models - if anthropic_request.thinking and anthropic_request.model.startswith("anthropic/"): + # Only include thinking field for Anthropic-compatible routes (native + # Anthropic models, and MiniMax models routed through the Anthropic- + # compatible endpoint). + is_anthropic_route = anthropic_request.model.startswith("anthropic/") or ( + anthropic_request.model.startswith("minimax/") + and MINIMAX_API_STYLE != "openai" + ) + if anthropic_request.thinking and is_anthropic_route: litellm_request["thinking"] = anthropic_request.thinking # Add optional parameters if present @@ -740,6 +794,8 @@ def convert_litellm_to_anthropic( clean_model = clean_model[len("anthropic/") :] elif clean_model.startswith("openai/"): clean_model = clean_model[len("openai/") :] + elif clean_model.startswith("minimax/"): + clean_model = clean_model[len("minimax/") :] # Check if this is a Claude model (which supports content blocks) is_claude_model = clean_model.startswith("claude-") @@ -1232,6 +1288,8 @@ async def create_message(request: MessagesRequest, raw_request: Request): clean_model = clean_model[len("anthropic/") :] elif clean_model.startswith("openai/"): clean_model = clean_model[len("openai/") :] + elif clean_model.startswith("minimax/"): + clean_model = clean_model[len("minimax/") :] logger.debug( f"πŸ“Š PROCESSING REQUEST: Model={request.model}, Stream={request.stream}" @@ -1251,6 +1309,18 @@ async def create_message(request: MessagesRequest, raw_request: Request): ) else: logger.debug(f"Using OpenAI API key for model: {request.model}") + elif request.model.startswith("minimax/"): + litellm_request["api_key"] = MINIMAX_API_KEY + if MINIMAX_API_STYLE == "openai": + litellm_request["api_base"] = MINIMAX_BASE_URL + logger.debug( + f"Using MiniMax API key and OpenAI-compatible base URL {MINIMAX_BASE_URL} for model: {request.model}" + ) + else: + litellm_request["api_base"] = MINIMAX_ANTHROPIC_BASE_URL + logger.debug( + f"Using MiniMax API key and Anthropic-compatible base URL {MINIMAX_ANTHROPIC_BASE_URL} for model: {request.model}" + ) elif request.model.startswith("gemini/"): if USE_VERTEX_AUTH: litellm_request["vertex_project"] = VERTEX_PROJECT @@ -1581,6 +1651,8 @@ async def count_tokens(request: TokenCountRequest, raw_request: Request): clean_model = clean_model[len("anthropic/") :] elif clean_model.startswith("openai/"): clean_model = clean_model[len("openai/") :] + elif clean_model.startswith("minimax/"): + clean_model = clean_model[len("minimax/") :] # Convert the messages to a format LiteLLM can understand converted_request = convert_anthropic_to_litellm( @@ -1622,6 +1694,12 @@ async def count_tokens(request: TokenCountRequest, raw_request: Request): # Add custom base URL for OpenAI models if configured if request.model.startswith("openai/") and OPENAI_BASE_URL: token_counter_args["api_base"] = OPENAI_BASE_URL + elif request.model.startswith("minimax/"): + token_counter_args["api_base"] = ( + MINIMAX_ANTHROPIC_BASE_URL + if MINIMAX_API_STYLE != "openai" + else MINIMAX_BASE_URL + ) # Count tokens token_count = token_counter(**token_counter_args) diff --git a/tests.py b/tests.py index 84d1b186..3ad9307e 100644 --- a/tests.py +++ b/tests.py @@ -170,7 +170,19 @@ {"role": "user", "content": "Count from 1 to 5, with one number per line."} ] }, - + + # MiniMax via the Anthropic-compatible endpoint. + # Proxy-only test: there is no native Anthropic equivalent to compare + # against, so the harness validates the proxy response format directly. + "minimax": { + "model": "minimax/MiniMax-M3", + "max_tokens": 300, + "proxy_only": True, + "messages": [ + {"role": "user", "content": "Hello! Can you tell me about Paris in 2-3 sentences?"} + ] + }, + # Tool use with streaming "calculator_stream": { "model": MODEL, @@ -361,6 +373,54 @@ def test_request(test_name, request_data, check_tools=False): traceback.print_exc() return False +def test_proxy_only(test_name, request_data): + """Run a proxy-only test for models without a native Anthropic equivalent. + + Some backends (e.g. MiniMax via its Anthropic-compatible endpoint) have no + corresponding model on the Anthropic API, so there is nothing to compare + against. For these scenarios the proxy response is validated directly. + """ + print(f"\n{'='*20} RUNNING PROXY-ONLY TEST: {test_name} {'='*20}") + + print(f"\nRequest data:\n{json.dumps({k: v for k, v in request_data.items() if k != 'messages'}, indent=2)}") + + # Send only the request body to the proxy, dropping harness-only flags + data = request_data.copy() + data.pop("proxy_only", None) + + try: + response = get_response(PROXY_API_URL, proxy_headers, data) + + if response.status_code != 200: + print(f"\n❌ Proxy returned status {response.status_code}: {response.text}") + return False + + proxy_json = response.json() + + # Validate the Anthropic-format response structure + assert proxy_json.get("role") == "assistant", "Proxy role is not 'assistant'" + assert proxy_json.get("type") == "message", "Proxy type is not 'message'" + assert "content" in proxy_json and isinstance(proxy_json["content"], list), ( + "Proxy content is not a list" + ) + assert len(proxy_json["content"]) > 0, "Proxy content is empty" + + # Require at least one non-empty text block + has_text = any( + item.get("type") == "text" and item.get("text") + for item in proxy_json["content"] + ) + assert has_text, "Proxy response contains no text content" + + print(f"\nβœ… Test {test_name} passed!") + return True + + except Exception as e: + print(f"\n❌ Error in test {test_name}: {str(e)}") + import traceback + traceback.print_exc() + return False + # ================= STREAMING TESTS ================= class StreamStats: @@ -643,20 +703,37 @@ async def run_tests(args): # Skip streaming tests if test_data.get("stream"): continue - + + # Skip proxy-only tests (run separately below) + if test_data.get("proxy_only"): + continue + # Skip tool tests if requested if args.simple and "tools" in test_data: continue - + # Skip non-tool tests if tools_only if args.tools_only and "tools" not in test_data: continue - + # Run the test check_tools = "tools" in test_data result = test_request(test_name, test_data, check_tools=check_tools) results[test_name] = result - + + # Run proxy-only tests (models without a native Anthropic equivalent) + if not args.streaming_only: + print("\n\n=========== RUNNING PROXY-ONLY TESTS ===========\n") + for test_name, test_data in TEST_SCENARIOS.items(): + if not test_data.get("proxy_only"): + continue + + if args.tools_only and "tools" not in test_data: + continue + + result = test_proxy_only(test_name, test_data) + results[test_name] = result + # Now run streaming tests if not args.no_streaming: print("\n\n=========== RUNNING STREAMING TESTS ===========\n") From 18c7707dbd73e96d7457fcada9091a50b3be19e7 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:39:39 +0800 Subject: [PATCH 2/2] fix: complete MiniMax routing defaults --- .env.example | 4 ++-- README.md | 26 ++++++++++++++++---------- server.py | 17 ++++++++++++----- tests.py | 10 +++++++++- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index 5b6c154c..16e43add 100644 --- a/.env.example +++ b/.env.example @@ -35,8 +35,8 @@ MINIMAX_API_STYLE="anthropic" # Example MiniMax mapping: # PREFERRED_PROVIDER="minimax" -# BIG_MODEL="MiniMax-M3" -# SMALL_MODEL="MiniMax-M2.7" +# BIG_MODEL="MiniMax-M3" # Optional, it's the default for MiniMax pref +# SMALL_MODEL="MiniMax-M2.7" # Optional, it's the default for MiniMax pref # Example Google with vertex AI auth via ADC: # PREFERRED_PROVIDER="google" diff --git a/README.md b/README.md index a4972432..26ddd684 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ A proxy server that lets you use Anthropic clients with Gemini, OpenAI, or Anthr * `VERTEX_PROJECT` (Optional): Your Google Cloud Project ID (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`). * `VERTEX_LOCATION` (Optional): The Google Cloud region for Vertex AI (e.g., `us-central1`) (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`). * `PREFERRED_PROVIDER` (Optional): Set to `openai` (default), `google`, `anthropic`, or `minimax`. This determines the primary backend for mapping `haiku`/`sonnet`. - * `BIG_MODEL` (Optional): The model to map `sonnet` requests to. Defaults to `gpt-4.1` (if `PREFERRED_PROVIDER=openai`) or `gemini-2.5-pro-preview-03-25`. Ignored when `PREFERRED_PROVIDER=anthropic`. - * `SMALL_MODEL` (Optional): The model to map `haiku` requests to. Defaults to `gpt-4.1-mini` (if `PREFERRED_PROVIDER=openai`) or `gemini-2.0-flash`. Ignored when `PREFERRED_PROVIDER=anthropic`. + * `BIG_MODEL` (Optional): The model to map `sonnet` requests to. Defaults to `gpt-4.1`, or `MiniMax-M3` when `PREFERRED_PROVIDER=minimax`. Ignored when `PREFERRED_PROVIDER=anthropic`. + * `SMALL_MODEL` (Optional): The model to map `haiku` requests to. Defaults to `gpt-4.1-mini`, or `MiniMax-M2.7` when `PREFERRED_PROVIDER=minimax`. Ignored when `PREFERRED_PROVIDER=anthropic`. * `MINIMAX_BASE_URL` (Optional): The MiniMax OpenAI-compatible endpoint. Defaults to `https://api.minimax.io/v1` (global); set to `https://api.minimaxi.com/v1` for the China region. * `MINIMAX_ANTHROPIC_BASE_URL` (Optional): The MiniMax Anthropic-compatible endpoint. Defaults to `https://api.minimax.io/anthropic` (global); set to `https://api.minimaxi.com/anthropic` for the China region. * `MINIMAX_API_STYLE` (Optional): Which MiniMax endpoint to route chat completions through. Defaults to `anthropic` (the Anthropic-compatible endpoint); set to `openai` to use the OpenAI-compatible endpoint. @@ -106,12 +106,12 @@ docker run -d --env-file .env -p 8082:8082 ghcr.io/1rgs/claude-code-proxy:latest ## Model Mapping πŸ—ΊοΈ -The proxy automatically maps Claude models to either OpenAI or Gemini models based on the configured model: +The proxy automatically maps Claude models to the configured provider: -| Claude Model | Default Mapping | When BIG_MODEL/SMALL_MODEL is a Gemini model | -|--------------|--------------|---------------------------| -| haiku | openai/gpt-4o-mini | gemini/[model-name] | -| sonnet | openai/gpt-4o | gemini/[model-name] | +| Claude Model | OpenAI default | Google mapping | MiniMax default | +|--------------|----------------|----------------|-----------------| +| haiku | openai/gpt-4.1-mini | gemini/[model-name] | minimax/MiniMax-M2.7 | +| sonnet | openai/gpt-4.1 | gemini/[model-name] | minimax/MiniMax-M3 | ### Supported Models @@ -135,11 +135,17 @@ The following Gemini models are supported with automatic `gemini/` prefix handli - gemini-2.5-pro - gemini-2.5-flash +#### MiniMax Models +The following MiniMax models are supported with automatic `minimax/` prefix handling: +- MiniMax-M3 +- MiniMax-M2.7 + ### Model Prefix Handling The proxy automatically adds the appropriate prefix to model names: - OpenAI models get the `openai/` prefix - Gemini models get the `gemini/` prefix -- The BIG_MODEL and SMALL_MODEL will get the appropriate prefix based on whether they're in the OpenAI or Gemini model lists +- MiniMax models get the `minimax/` prefix +- The BIG_MODEL and SMALL_MODEL use the prefix for the configured provider For example: - `gpt-4o` becomes `openai/gpt-4o` @@ -194,8 +200,8 @@ PREFERRED_PROVIDER="anthropic" ```dotenv MINIMAX_API_KEY="your-minimax-key" PREFERRED_PROVIDER="minimax" -BIG_MODEL="MiniMax-M3" -SMALL_MODEL="MiniMax-M2.7" +# BIG_MODEL="MiniMax-M3" # Optional, it's the default for MiniMax pref +# SMALL_MODEL="MiniMax-M2.7" # Optional, it's the default for MiniMax pref # MINIMAX_ANTHROPIC_BASE_URL="https://api.minimax.io/anthropic" # Optional, it's the default (global) # MINIMAX_BASE_URL="https://api.minimax.io/v1" # Optional, the OpenAI-compatible endpoint # MINIMAX_API_STYLE="anthropic" # Optional, it's the default; set to "openai" for the OpenAI-compatible endpoint diff --git a/server.py b/server.py index defd6928..49793b13 100644 --- a/server.py +++ b/server.py @@ -116,9 +116,12 @@ def format(self, record): PREFERRED_PROVIDER = os.environ.get("PREFERRED_PROVIDER", "openai").lower() # Get model mapping configuration from environment -# Default to latest OpenAI models if not set -BIG_MODEL = os.environ.get("BIG_MODEL", "gpt-4.1") -SMALL_MODEL = os.environ.get("SMALL_MODEL", "gpt-4.1-mini") +DEFAULT_BIG_MODEL = "MiniMax-M3" if PREFERRED_PROVIDER == "minimax" else "gpt-4.1" +DEFAULT_SMALL_MODEL = ( + "MiniMax-M2.7" if PREFERRED_PROVIDER == "minimax" else "gpt-4.1-mini" +) +BIG_MODEL = os.environ.get("BIG_MODEL", DEFAULT_BIG_MODEL) +SMALL_MODEL = os.environ.get("SMALL_MODEL", DEFAULT_SMALL_MODEL) # List of OpenAI models OPENAI_MODELS = [ @@ -222,7 +225,9 @@ class Tool(BaseModel): class ThinkingConfig(BaseModel): - enabled: bool = True + type: Optional[str] = None + enabled: Optional[bool] = None + budget_tokens: Optional[int] = None class MessagesRequest(BaseModel): @@ -706,7 +711,9 @@ def convert_anthropic_to_litellm(anthropic_request: MessagesRequest) -> Dict[str and MINIMAX_API_STYLE != "openai" ) if anthropic_request.thinking and is_anthropic_route: - litellm_request["thinking"] = anthropic_request.thinking + litellm_request["thinking"] = anthropic_request.thinking.model_dump( + exclude_none=True + ) # Add optional parameters if present if anthropic_request.stop_sequences: diff --git a/tests.py b/tests.py index 3ad9307e..f58e650f 100644 --- a/tests.py +++ b/tests.py @@ -178,6 +178,8 @@ "model": "minimax/MiniMax-M3", "max_tokens": 300, "proxy_only": True, + "requires_env": "MINIMAX_API_KEY", + "thinking": {"type": "adaptive"}, "messages": [ {"role": "user", "content": "Hello! Can you tell me about Paris in 2-3 sentences?"} ] @@ -387,6 +389,7 @@ def test_proxy_only(test_name, request_data): # Send only the request body to the proxy, dropping harness-only flags data = request_data.copy() data.pop("proxy_only", None) + data.pop("requires_env", None) try: response = get_response(PROXY_API_URL, proxy_headers, data) @@ -728,6 +731,11 @@ async def run_tests(args): if not test_data.get("proxy_only"): continue + required_env = test_data.get("requires_env") + if required_env and not os.environ.get(required_env): + print(f"Skipping {test_name}: {required_env} is not set") + continue + if args.tools_only and "tools" not in test_data: continue @@ -790,4 +798,4 @@ async def main(): sys.exit(0 if success else 1) if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main())