mimo2codex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +124 -0
- package/LICENSE +21 -0
- package/README.md +536 -0
- package/README.zh.md +750 -0
- package/dist/cli.js +202 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +86 -0
- package/dist/config.js.map +1 -0
- package/dist/server.js +161 -0
- package/dist/server.js.map +1 -0
- package/dist/translate/reqToChat.js +381 -0
- package/dist/translate/reqToChat.js.map +1 -0
- package/dist/translate/respToResponses.js +94 -0
- package/dist/translate/respToResponses.js.map +1 -0
- package/dist/translate/streamToSse.js +352 -0
- package/dist/translate/streamToSse.js.map +1 -0
- package/dist/translate/types.js +4 -0
- package/dist/translate/types.js.map +1 -0
- package/dist/upstream/chatStream.js +56 -0
- package/dist/upstream/chatStream.js.map +1 -0
- package/dist/upstream/mimoClient.js +99 -0
- package/dist/upstream/mimoClient.js.map +1 -0
- package/dist/util/ids.js +19 -0
- package/dist/util/ids.js.map +1 -0
- package/dist/util/log.js +32 -0
- package/dist/util/log.js.map +1 -0
- package/dist/util/sse.js +61 -0
- package/dist/util/sse.js.map +1 -0
- package/mimoskill/SKILL.md +145 -0
- package/mimoskill/assets/pet_prompt_template.md +94 -0
- package/mimoskill/references/models.md +111 -0
- package/mimoskill/references/pet_workflow.md +197 -0
- package/mimoskill/scripts/generate_pet.py +365 -0
- package/mimoskill/scripts/install_pet.sh +220 -0
- package/mimoskill/scripts/mimo_chat.py +199 -0
- package/package.json +69 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
mimo_chat.py — single-shot or streaming chat with Xiaomi MiMo V2.5.
|
|
4
|
+
|
|
5
|
+
Hits MiMo's OpenAI-compatible /v1/chat/completions endpoint directly. Handles
|
|
6
|
+
the MiMo-specific quirks:
|
|
7
|
+
|
|
8
|
+
- max_completion_tokens (not max_tokens)
|
|
9
|
+
- vision via mimo-v2.5 / mimo-v2-omni (and the required text part next to
|
|
10
|
+
image_url, otherwise MiMo 400s with "text is not set")
|
|
11
|
+
- web_search builtin tool (requires Web Search Plugin activated in console)
|
|
12
|
+
- reasoning_content extraction
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
export MIMO_API_KEY=sk-xxxx
|
|
16
|
+
python3 mimo_chat.py "your prompt"
|
|
17
|
+
python3 mimo_chat.py --model mimo-v2.5 --image https://x/y.png "describe"
|
|
18
|
+
python3 mimo_chat.py --search "今天上海天气?"
|
|
19
|
+
python3 mimo_chat.py --stream "tell me a story"
|
|
20
|
+
|
|
21
|
+
Only depends on the standard library — no `openai` SDK install needed.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.request
|
|
30
|
+
import urllib.error
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_messages(prompt: str, image: str | None) -> list[dict[str, Any]]:
|
|
35
|
+
if image is None:
|
|
36
|
+
return [{"role": "user", "content": prompt}]
|
|
37
|
+
# MiMo requires BOTH image_url and a text part — sending image-only returns
|
|
38
|
+
# 400 "Param Incorrect: `text` is not set". If the user gave no prompt,
|
|
39
|
+
# fall back to a single space (the model will infer intent from the image).
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
"role": "user",
|
|
43
|
+
"content": [
|
|
44
|
+
{"type": "image_url", "image_url": {"url": image}},
|
|
45
|
+
{"type": "text", "text": prompt or " "},
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_body(
|
|
52
|
+
*,
|
|
53
|
+
prompt: str,
|
|
54
|
+
image: str | None,
|
|
55
|
+
model: str,
|
|
56
|
+
stream: bool,
|
|
57
|
+
search: bool,
|
|
58
|
+
max_tokens: int,
|
|
59
|
+
temperature: float,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
body: dict[str, Any] = {
|
|
62
|
+
"model": model,
|
|
63
|
+
"messages": build_messages(prompt, image),
|
|
64
|
+
"max_completion_tokens": max_tokens,
|
|
65
|
+
"temperature": temperature,
|
|
66
|
+
"stream": stream,
|
|
67
|
+
}
|
|
68
|
+
if search:
|
|
69
|
+
# MiMo native web_search builtin. Requires the Web Search Plugin to
|
|
70
|
+
# be activated at https://platform.xiaomimimo.com/#/console/plugin.
|
|
71
|
+
body["tools"] = [{"type": "web_search", "force_search": True}]
|
|
72
|
+
body["tool_choice"] = "auto"
|
|
73
|
+
return body
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def post(url: str, body: dict[str, Any], api_key: str, stream: bool) -> Any:
|
|
77
|
+
req = urllib.request.Request(
|
|
78
|
+
url,
|
|
79
|
+
method="POST",
|
|
80
|
+
data=json.dumps(body).encode("utf-8"),
|
|
81
|
+
headers={
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"Accept": "text/event-stream" if stream else "application/json",
|
|
84
|
+
"Authorization": f"Bearer {api_key}",
|
|
85
|
+
"User-Agent": "mimoskill/0.1",
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
return urllib.request.urlopen(req, timeout=300)
|
|
90
|
+
except urllib.error.HTTPError as e:
|
|
91
|
+
snippet = e.read().decode("utf-8", "replace")
|
|
92
|
+
sys.stderr.write(f"MiMo returned HTTP {e.code}: {snippet}\n")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
except urllib.error.URLError as e:
|
|
95
|
+
sys.stderr.write(f"connection failed: {e}\n")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def stream_chat(resp: Any) -> None:
|
|
100
|
+
annotations: list[dict[str, Any]] = []
|
|
101
|
+
for raw in resp:
|
|
102
|
+
line = raw.decode("utf-8", "replace").strip()
|
|
103
|
+
if not line.startswith("data:"):
|
|
104
|
+
continue
|
|
105
|
+
data = line[5:].strip()
|
|
106
|
+
if data == "[DONE]":
|
|
107
|
+
break
|
|
108
|
+
try:
|
|
109
|
+
chunk = json.loads(data)
|
|
110
|
+
except json.JSONDecodeError:
|
|
111
|
+
continue
|
|
112
|
+
choice = chunk.get("choices", [{}])[0]
|
|
113
|
+
delta = choice.get("delta", {})
|
|
114
|
+
for ann in delta.get("annotations") or []:
|
|
115
|
+
annotations.append(ann)
|
|
116
|
+
# Print reasoning_content dimly to stderr, content to stdout
|
|
117
|
+
if r := delta.get("reasoning_content"):
|
|
118
|
+
sys.stderr.write(r)
|
|
119
|
+
sys.stderr.flush()
|
|
120
|
+
if c := delta.get("content"):
|
|
121
|
+
sys.stdout.write(c)
|
|
122
|
+
sys.stdout.flush()
|
|
123
|
+
sys.stdout.write("\n")
|
|
124
|
+
if annotations:
|
|
125
|
+
sys.stderr.write("\n--- citations ---\n")
|
|
126
|
+
for a in annotations:
|
|
127
|
+
sys.stderr.write(f" • {a.get('title', '(no title)')}\n {a.get('url')}\n")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def non_stream_chat(resp: Any) -> None:
|
|
131
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
132
|
+
msg = payload["choices"][0]["message"]
|
|
133
|
+
if reasoning := msg.get("reasoning_content"):
|
|
134
|
+
sys.stderr.write(f"[reasoning]\n{reasoning}\n[/reasoning]\n\n")
|
|
135
|
+
print(msg.get("content") or "")
|
|
136
|
+
if anns := msg.get("annotations"):
|
|
137
|
+
sys.stderr.write("\n--- citations ---\n")
|
|
138
|
+
for a in anns:
|
|
139
|
+
sys.stderr.write(f" • {a.get('title', '(no title)')}\n {a.get('url')}\n")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> None:
|
|
143
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
144
|
+
p.add_argument("prompt", nargs="?", default="", help="user message text")
|
|
145
|
+
p.add_argument("--model", default=os.environ.get("MIMO_MODEL", "mimo-v2.5-pro"))
|
|
146
|
+
p.add_argument("--image", help="image URL to attach (forces vision-capable model)")
|
|
147
|
+
p.add_argument("--search", action="store_true", help="enable MiMo web_search builtin")
|
|
148
|
+
p.add_argument("--stream", action="store_true", help="stream the response")
|
|
149
|
+
p.add_argument("--max-tokens", type=int, default=2048)
|
|
150
|
+
p.add_argument("--temperature", type=float, default=0.7)
|
|
151
|
+
p.add_argument(
|
|
152
|
+
"--base-url",
|
|
153
|
+
default=os.environ.get("MIMO_BASE_URL", "https://api.xiaomimimo.com/v1"),
|
|
154
|
+
help="set to https://token-plan-cn.xiaomimimo.com/v1 for tp-* keys",
|
|
155
|
+
)
|
|
156
|
+
args = p.parse_args()
|
|
157
|
+
|
|
158
|
+
api_key = os.environ.get("MIMO_API_KEY")
|
|
159
|
+
if not api_key:
|
|
160
|
+
sys.stderr.write("error: MIMO_API_KEY not set in environment\n")
|
|
161
|
+
sys.stderr.write(
|
|
162
|
+
" get one at https://platform.xiaomimimo.com/#/console/api-keys\n"
|
|
163
|
+
)
|
|
164
|
+
sys.exit(2)
|
|
165
|
+
|
|
166
|
+
if not args.prompt and not args.image:
|
|
167
|
+
sys.stderr.write("error: pass a prompt and/or --image\n")
|
|
168
|
+
sys.exit(2)
|
|
169
|
+
|
|
170
|
+
# Auto-bump to a vision model if user passed --image with a non-vision model
|
|
171
|
+
model = args.model
|
|
172
|
+
if args.image and "omni" not in model.lower() and not model.startswith("mimo-v2.5["):
|
|
173
|
+
if model != "mimo-v2.5":
|
|
174
|
+
sys.stderr.write(
|
|
175
|
+
f"note: --image given but model is '{model}' which doesn't see images.\n"
|
|
176
|
+
f" switching to mimo-v2.5 for this call.\n"
|
|
177
|
+
)
|
|
178
|
+
model = "mimo-v2.5"
|
|
179
|
+
|
|
180
|
+
body = build_body(
|
|
181
|
+
prompt=args.prompt,
|
|
182
|
+
image=args.image,
|
|
183
|
+
model=model,
|
|
184
|
+
stream=args.stream,
|
|
185
|
+
search=args.search,
|
|
186
|
+
max_tokens=args.max_tokens,
|
|
187
|
+
temperature=args.temperature,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
url = args.base_url.rstrip("/") + "/chat/completions"
|
|
191
|
+
resp = post(url, body, api_key, args.stream)
|
|
192
|
+
if args.stream:
|
|
193
|
+
stream_chat(resp)
|
|
194
|
+
else:
|
|
195
|
+
non_stream_chat(resp)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mimo2codex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local proxy that lets the latest OpenAI Codex CLI / desktop talk to Xiaomi MiMo (V2.5 Pro) via the Responses API by translating to Chat Completions on the fly.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codex",
|
|
7
|
+
"codex-cli",
|
|
8
|
+
"openai",
|
|
9
|
+
"mimo",
|
|
10
|
+
"xiaomi",
|
|
11
|
+
"xiaomimimo",
|
|
12
|
+
"mimo-v2.5",
|
|
13
|
+
"responses-api",
|
|
14
|
+
"chat-completions",
|
|
15
|
+
"proxy",
|
|
16
|
+
"adapter",
|
|
17
|
+
"router",
|
|
18
|
+
"cc-switch",
|
|
19
|
+
"llm"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"mimo2codex": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/server.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"mimoskill",
|
|
29
|
+
"AGENTS.md",
|
|
30
|
+
"README.md",
|
|
31
|
+
"README.zh.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p .",
|
|
39
|
+
"start": "node dist/cli.js",
|
|
40
|
+
"dev": "tsx src/cli.ts",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"prepublishOnly": "npm run build && npm test",
|
|
45
|
+
"release:patch": "npm version patch && npm publish && git push --follow-tags",
|
|
46
|
+
"release:minor": "npm version minor && npm publish && git push --follow-tags",
|
|
47
|
+
"release:major": "npm version major && npm publish && git push --follow-tags"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"eventsource-parser": "^3.0.0",
|
|
51
|
+
"nanoid": "^5.0.7"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.11.0",
|
|
55
|
+
"tsx": "^4.7.0",
|
|
56
|
+
"typescript": "^5.4.0",
|
|
57
|
+
"vitest": "^1.4.0"
|
|
58
|
+
},
|
|
59
|
+
"author": "7as0nch",
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"homepage": "https://github.com/7as0nch/mimo2codex#readme",
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "git+https://github.com/7as0nch/mimo2codex.git"
|
|
65
|
+
},
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/7as0nch/mimo2codex/issues"
|
|
68
|
+
}
|
|
69
|
+
}
|