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.
@@ -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
+ }