Reverse-engineer MiniMax Agent web chat into an OpenAI-compatible API proxy
Powered by webai2api skill
🇨🇳 中文版(主文档) — Click for Chinese docs (primary)
- Overview
- Quick Start
- How It Works (Must Read)
- Configuration Deep Dive
- API Reference
- Project Structure
- FAQ
- License
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.
| 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 |
- Python 3.10+
- A MiniMax Agent account
- A HAR file exported from
agent.minimaxi.com
F12 → Network → Check "Preserve log" → Send a message → Right-click "Save all as HAR with content"
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.shcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"MiniMax-M3","messages":[{"role":"user","content":"Hello!"}],"stream":false}'To understand this project, you first need to understand how the MiniMax web frontend calls its own API.
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.
How
sign.pywas 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)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 = 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 JSURLSearchParamsinsertion order)body_json=JSON.stringify(body_obj)— same body as x-signaturetimestamp_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.
┌─ 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"}}]} │
└─────────────────────────────────────────┘
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:
- Puts the token in the URL (
?token=...) - Puts the token in the headers (
token: ...)
Exactly matching the browser's behavior.
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.
| 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 | ✘ |
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 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": "..."}Fully OpenAI-compatible.
data: {"choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"choices":[{"delta":{"content":"Hello"},"index":0}]}
data: {"choices":[{"delta":{},"finish_reason":"stop","index":0}]}
data: [DONE]
{
"choices": [{"message": {"role": "assistant", "content": "Hello!"}}]
}Pass tools and tool_choice to enable function calling. The model responds with DSML tags, parsed into OpenAI tool_calls format.
{
"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
}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]
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_xxx",
"type": "function",
"function": {
"name": "move",
"arguments": "{\"target\": \"kitchen\"}"
}
}]
},
"finish_reason": "tool_calls"
}]
}Request (with tools) → adapter injects DSML → MiniMax generates <|DSML|invoke> tags
→ StreamSieve separates tags → tool_dsml.py parses → OpenAI tool_calls response
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 | 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 |
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
│ │ │
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.
GNU General Public License v3.0 — see LICENSE.