Skip to content

Latest commit

 

History

History
460 lines (359 loc) · 16.3 KB

File metadata and controls

460 lines (359 loc) · 16.3 KB

🚀 MiniMax Agent Web2API

Reverse-engineer MiniMax Agent web chat into an OpenAI-compatible API proxy
Powered by webai2api skill

Python FastAPI License

🇨🇳 中文版(主文档) — Click for Chinese docs (primary)


📋 Table of Contents


🔭 Overview

This project reverse-engineers the JavaScript signing algorithm from MiniMax Agent's (agent.minimaxi.com) frontend, and transforms the web chat interface into a fully OpenAI-compatible REST API proxy.

Core Capabilities

Capability Status Description
Dynamic Signing Reverse-engineered JS signing algorithm — any message, real-time signing
Tool Calling (DSML) OpenAI-format function calling via DSML injection
Streaming / Non-streaming SSE streaming and one-shot responses

⚡ Quick Start

Prerequisites

  • Python 3.10+
  • A MiniMax Agent account
  • A HAR file exported from agent.minimaxi.com

Capturing a HAR File

F12 → Network → Check "Preserve log" → Send a message → Right-click "Save all as HAR with content"

Setup & Launch

Windows:

Double-click start-config-tool.bat → Select HAR → Parse → Save to .env
Double-click start.bat

Linux/macOS:

chmod +x start-config-tool.sh && ./start-config-tool.sh
chmod +x start.sh && ./start.sh

Verify

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"MiniMax-M3","messages":[{"role":"user","content":"Hello!"}],"stream":false}'

🔬 How It Works (Must Read)

To understand this project, you first need to understand how the MiniMax web frontend calls its own API.

MiniMax's Request Signing

Every API request from the MiniMax frontend includes four special headers, automatically generated by Webpack module 97516 (function L()) in the JavaScript bundle:

Header Example Description
token eyJhbGciOiJIUzI1NiIs... JWT authentication token
x-timestamp 1780470413 Unix timestamp (seconds)
x-signature 2abd46524d558934... 32-char hex string, computed from the request body
yy 98b376ad1900c0a6... 32-char hex string, computed from the full URL + body

These headers are generated automatically by the frontend — the user never sees them.

The Reverse Engineering Process

How sign.py was born

We downloaded all JS chunks from MiniMax's CDN and located the critical Webpack modules:

Module ID Function Source File
97516 Main signing function L() page chunk
52724 yy computation page chunk
65776 Client metadata generation (URL params) page chunk
96467 MD5 hash implementation vendor chunk

From module 97516 we extracted:

// x-signature: MD5(timestamp_seconds + static_secret + request_body)
w["x-signature"] = i()(`${a}I*7Cf%WZ#S&%1RlZJ&C2${v}`)
// a = seconds timestamp, v = JSON.stringify(body)
// yy: MD5(encodeURIComponent(full_url_with_params) + "_" + body + MD5(ms_timestamp) + "ooui")
let l = `${encodeURIComponent(t)}_${o}${i()(a.toString())}ooui`;
return i()(l)

The Signing Algorithms

x-signature (Verified, 100% accurate)

x-signature = MD5(
    timestamp_seconds +       # Current Unix timestamp (seconds)
    "I*7Cf%WZ#S&%1RlZJ&C2" + # Hardcoded static key (extracted from JS)
    body_string               # JSON.stringify'd request body
)
  • URL-independent: signature depends only on timestamp + secret + body
  • Verified: recomputed against original HAR timestamps — 100% match

yy (Algorithm correct, needs runtime params)

yy = MD5(
    encodeURIComponent(full_url_with_params) + "_" +
    body_json +
    MD5(str(timestamp_ms)) +
    "ooui"
)
  • full_url_with_params = API path + all client metadata params (serialized in JS URLSearchParams insertion order)
  • body_json = JSON.stringify(body_obj) — same body as x-signature
  • timestamp_ms = millisecond timestamp
  • "ooui" = fixed suffix string

yy requires browser runtime parameters (UUID, device_id, user_id, screen dimensions, etc.). These are extracted from the HAR file's URL query string and stored in .env.

Request Flow

┌─ Your App ──────────────────────────────┐
│  POST /v1/chat/completions               │
│  {"messages":[{"role":"user",            │
│    "content":"Hello!"}],"stream":true}    │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─ server.py ────┬─────────────────────────┐
│  ① Parse OpenAI-format request           │
│  ② Call adapter.py                       │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─ adapter.py ───┬─────────────────────────┐
│  ③ Build MiniMax message body            │
│     {"content":"Hello!",                  │
│      "model":{...},                       │
│      "turn_id":"xxx",                     │
│      "worktreeMode":false}                │
│                                           │
│  ④ Call sign_request() to sign           │
│     sign.py ──────────────────┐           │
│       x-timestamp = now()     │           │
│       x-signature = MD5(     │           │
│         ts + secret + body)  │           │
│       yy = MD5(             │           │
│         enc_url + "_" +     │           │
│         body + md5(ts_ms) + │           │
│         "ooui")             │           │
│     ←────────────────────────┘           │
│                                           │
│  ⑤ Send request to MiniMax               │
│     POST /session/{id}/message            │
│     Headers: token, x-timestamp,          │
│              x-signature, yy              │
│     Body: message body                    │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─ MiniMax ──────┬─────────────────────────┐
│  ⑥ Signature verified ✅                  │
│  ⑦ SSE streaming response                │
│     data:{"type":6,"agent_message_chunk": │
│       {"msg_content":"Hello"}}            │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─ adapter.py ───┬─────────────────────────┐
│  ⑧ Parse SSE, extract msg_content        │
│  ⑨ Assemble OpenAI-format response       │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─ Your App ──────────────────────────────┐
│  {"choices":[{"delta":                  │
│    {"content":"Hello"}}]}               │
└─────────────────────────────────────────┘

Why no Cookie?

Traditional approaches rely on cookies for authentication. MiniMax's API uses JWT Token authentication — the token is placed directly in the URL query string and HTTP headers, independent of cookies. This project:

  1. Puts the token in the URL (?token=...)
  2. Puts the token in the headers (token: ...)

Exactly matching the browser's behavior.

About the Signing Secret

The static key I*7Cf%WZ#S&%1RlZJ&C2 in sign.py is extracted from MiniMax's public JavaScript bundle. It ships with the frontend code and is accessible to any browser via DevTools — it is not sensitive. This is standard practice for client-side API keys.


⚙ Configuration Deep Dive

.env — Generated by config_tool.py

Variable HAR Source Purpose Required
TOKEN URL query token JWT auth token, placed in both headers and URL
AGENT_ID URL path /agent/{id}/session Which Agent to invoke
UUID URL query uuid Browser fingerprint; used for yy signing
DEVICE_ID URL query device_id Device ID; used for yy signing
USER_ID URL query user_id User ID; used for yy signing
SCREEN_WIDTH URL query screen_width Screen width; used for yy signing
SCREEN_HEIGHT URL query screen_height Screen height; used for yy signing
TARGET_URL URL scheme + host MiniMax base URL
STREAM_URL SSE request URL host MiniMax streaming URL
USER_AGENT Request header User-Agent string

What config_tool.py Does

Open HAR file
  ↓
Scan all requests, find:
  ├─ Session creation (POST /agent/{id}/session)
  └─ SSE message request (Content-Type: text/event-stream)
  ↓
Extract from URL:
  ├─ token, agent_id
  ├─ uuid, device_id, user_id
  ├─ screen_width, screen_height
  └─ stream_url
  ↓
Write to .env

sign.py Needs No Configuration

sign.py is a pure function library — zero external dependencies, zero config files. It receives all inputs through function parameters:

from sign import sign_request

result = sign_request(
    body_str='{"content":"Hello"}',      # Request body JSON string
    token="eyJ...",                      # JWT Token
    url_path="/archon/api/v1/session/xxx/message",  # API path
    uuid="...", device_id="...",         # Browser params
    user_id=..., screen_width=...,
    screen_height=...,
)
# Returns: {"x-timestamp": "...", "x-signature": "...", "yy": "..."}

📖 API Reference

POST /v1/chat/completions

Fully OpenAI-compatible.

Streaming (SSE)

data: {"choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"choices":[{"delta":{"content":"Hello"},"index":0}]}
data: {"choices":[{"delta":{},"finish_reason":"stop","index":0}]}
data: [DONE]

Non-Streaming

{
  "choices": [{"message": {"role": "assistant", "content": "Hello!"}}]
}

Tool Calling (Function Calling)

Pass tools and tool_choice to enable function calling. The model responds with DSML tags, parsed into OpenAI tool_calls format.

Request Example

{
  "model": "MiniMax-M3",
  "messages": [
    {"role": "system", "content": "You are a catgirl in the bedroom."},
    {"role": "user", "content": "I'm hungry, take me to the kitchen"}
  ],
  "tools": [{
    "type": "function",
    "function": {
      "name": "move",
      "description": "Move to a location",
      "parameters": {
        "type": "object",
        "properties": {
          "target": {"type": "string", "description": "Target location"}
        },
        "required": ["target"]
      }
    }
  }],
  "tool_choice": "auto",
  "stream": true
}

Streaming Response (Tool Calls)

data: {"choices":[{"delta":{"role":"assistant","content":null},"index":0}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_xxx","type":"function","function":{"name":"move","arguments":"{\"target\":\"kitchen\"}"}}]},"index":0}]}
data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}
data: [DONE]

Non-Streaming Response (Tool Calls)

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_xxx",
        "type": "function",
        "function": {
          "name": "move",
          "arguments": "{\"target\": \"kitchen\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

How It Works

Request (with tools) → adapter injects DSML → MiniMax generates <|DSML|invoke> tags
→ StreamSieve separates tags → tool_dsml.py parses → OpenAI tool_calls response

GET /v1/models / GET /health


📁 Project Structure

web2api/
├── sign.py             ← Reverse-engineered signing algorithm (pure functions, zero config)
├── tool_dsml.py        ← DSML tool call parser (inject + parse + convert)
├── tool_sieve.py       ← StreamSieve streaming tag separation engine
├── adapter.py          ← MiniMax API adapter (signing + DSML)
├── server.py           ← FastAPI proxy server (OpenAI-compatible API)
├── config_tool.py      ← GUI config tool (HAR → .env)
├── .env                ← Config file (generated by config_tool.py)
├── .env.example        ← Config template
├── start.bat / start.sh                     ← One-click server launcher
├── start-config-tool.bat / start-config-tool.sh  ← One-click config tool launcher
├── README.md / README.en.md
└── LICENSE

File Responsibilities

File Responsibility Needs Config?
sign.py Pure algorithm: input → signatures ❌ No config needed
adapter.py Business logic: build request → sign → send → parse SSE ❌ Reads from .env
server.py HTTP server: receive OpenAI format → delegate to adapter ❌ Reads from .env
config_tool.py Utility: HAR → extract params → write .env ❌ User just picks a HAR file
.env Configuration storage ✅ Generated by config_tool.py

Data Flow

config_tool.py          sign.py                adapter.py
    │                     │                       │
    │  Parse HAR          │                       │
    │  Extract token,     │  Pure functions,      │  Read .env
    │  uuid, device_id    │  stateless            │  Build message body
    │  etc.               │  input→output         │  Call sign.py
    │     │               │     │                 │  Send HTTP request
    │     ▼               │     ▼                 │  Parse SSE
    │  .env ──────────────┼─────┼────────────────▶│  Return OpenAI format
    │                     │                       │

🐛 FAQ

Q: What is x-signature? A: A request signature computed as MD5(timestamp + static_secret + body). This project implements it in sign.py.

Q: What is yy? A: A second signature: MD5(encodeURIComponent(full_url) + "_" + body + MD5(ms_timestamp) + "ooui"). Needs browser runtime parameters (UUID, device_id, etc.).

Q: Does sign.py need configuration? A: No. It is a pure function library with no external dependencies.

Q: Why do I need a HAR file? A: To extract the JWT token and browser parameters (UUID, device_id, etc.) needed for yy signing.

Q: What if my token expires? A: Re-capture a HAR file and run config_tool.py to update .env.

Q: Does this support function calling / tool calling? A: Yes, via DSML injection. Pass tools and tool_choice in your request — the response will contain standard OpenAI tool_calls. See API Reference - Tool Calling.


📄 License

GNU General Public License v3.0 — see LICENSE.


🇨🇳 中文版(主文档)