agent: MiMo Code — Xiaomi's fork of OpenCode, sharing its config shape (#98)

* agent: MiMo Code — a terminal coding assistant that is Xiaomi's fork of OpenCode, whose config keeps OpenCode's shape: its own mimocode.json(c) under ~/.config/mimocode, its auth beside it, and a magpie provider block with npm/@ai-sdk/openai-compatible, model and small_model fields. opencode() becomes openCodeLike() for the two to share it, and magpieProviderJSON splits shape from the catalog id (magpieProviderJSONFor), so a visibility narrowed on mimocode (magpie visible mimocode …) narrows its list and leaves OpenCode's alone. The library gives it the same instructions, MCP servers (its mcp key) and skills as OpenCode, except the shared skills go into its own skills/ folder: it reads Claude Code's skills only behind a flag, though it reads its CLAUDE.md like OpenCode, and the page says so. Its requests are told by the mimocode/ User-Agent. Checked against XiaomiMiMo/MiMo-Code

* icon: MiMo Code's own favicon, from packages/ui/src/assets/favicon — its frame with the bottom bar, in the monochrome the app masks to the text colour
This commit is contained in:
smallnest authored and GitHub committed 2026-09-27 08:42:44 +08:00
1 parent e0cc477990
commit 03a01a548d
15 files changed
+108 -26

No files matched your search

+4 -1
View File
@@ -19,6 +19,7 @@ and there is a terminal version (`magpie tui`) and a plain CLI.
Codex gpt-6-astra effort medium
Gemini CLI gemini-3.1-pro
OpenCode anthropic/claude-sonnet-5 small anthropic/claude-haiku-4-5
MiMo Code anthropic/claude-sonnet-5
Pi openrouter/z-ai/glm-5.2:batch
Goose anthropic/claude-sonnet-5
Cursor auto
@@ -68,6 +69,7 @@ and there is a terminal version (`magpie tui`) and a plain CLI.
| Codex | `~/.codex/config.toml` | provider, model, effort |
| Gemini CLI | `~/.gemini/settings.json`, `~/.gemini/.env` | auth, model |
| OpenCode | `~/.config/opencode/opencode.json(c)` | model, small |
| MiMo Code | `~/.config/mimocode/mimocode.json(c)` | model, small |
| Pi | `~/.pi/agent/settings.json` | model |
| Goose | `~/.config/goose/config.yaml` | model |
| Cursor CLI | `~/.cursor/cli-config.json` | model |
@@ -85,7 +87,7 @@ and there is a terminal version (`magpie tui`) and a plain CLI.
| ZCode | `~/.zcode/v2/config.json` | provider (magpie's models in ZCode's picker) |
| Alma | Alma's local API (`localhost:23001`, while Alma runs) | model (Alma's default; magpie's models as a provider) |
Provider-scoped agents (OpenCode, Pi, Goose, Crush, omp, Hermes Agent) take `provider/model`.
Provider-scoped agents (OpenCode, MiMo Code, Pi, Goose, Crush, omp, Hermes Agent) take `provider/model`.
Only agents that are installed or configured are shown.
## Providers and the gateway
@@ -359,6 +361,7 @@ magpie claude haiku "" # back to the main model
magpie gemini auth api-key
magpie opencode anthropic/claude-sonnet-5
magpie oc small anthropic/claude-haiku-4-5
magpie mimo anthropic/claude-sonnet-5
magpie save work # snapshot everything as a profile
magpie use work # switch back
+38 -16
View File
@@ -53,6 +53,7 @@ func All() []*Agent {
codex(home),
gemini(home),
opencode(home, cfg),
mimocode(home, cfg),
pi(home),
goose(home, cfg),
cursor(home),
@@ -187,7 +188,14 @@ func ownOptions(authFile string, cur string, extra ...string) []Option {
// magpieProviderJSON is the provider block agents with JSON configs get.
func magpieProviderJSON(shape string) any {
models := magpieModels(shape) // the shape is the agent's
return magpieProviderJSONFor(shape, shape)
}
// magpieProviderJSONFor is magpieProviderJSON for an agent whose catalog is
// narrowed under a different id than its config shape (mimocode shares
// OpenCode's shape but is seen as itself by magpie visible).
func magpieProviderJSONFor(shape, catalog string) any {
models := magpieModels(catalog) // the catalog is the agent's
switch shape {
case "opencode":
ms := map[string]any{}
@@ -268,16 +276,18 @@ func piThinkingLevels(efforts []string) map[string]any {
return levels
}
func opencode(home, cfg string) *Agent {
dir := filepath.Join(cfg, "opencode")
path := filepath.Join(dir, "opencode.json")
if _, err := os.Stat(filepath.Join(dir, "opencode.jsonc")); err == nil {
path = filepath.Join(dir, "opencode.jsonc")
// openCodeLike is OpenCode and the forks that keep its config shape
// (mimocode): a provider block with npm/@ai-sdk/openai-compatible, model and
// small_model fields, and its own auth file beside its config.
func openCodeLike(id, name, icon, bin, dir, auth string, ua []string, aliases ...string) *Agent {
path := filepath.Join(dir, id+".json")
if _, err := os.Stat(filepath.Join(dir, id+".jsonc")); err == nil {
path = filepath.Join(dir, id+".jsonc")
}
auth := filepath.Join(home, ".local", "share", "opencode", "auth.json")
provider := func() any { return magpieProviderJSONFor("opencode", id) }
opts := func(key string) func(map[string]string) []Option {
return func(cur map[string]string) []Option {
return append(ownOptions(auth, cur[key]), viaMagpie("opencode", magpieID+"/")...)
return append(ownOptions(auth, cur[key]), viaMagpie(id, magpieID+"/")...)
}
}
get := func(k string) string { v, _ := edit.GetJSON(path, k); return v }
@@ -299,7 +309,7 @@ func opencode(home, cfg string) *Agent {
if own := ownGatewayProvider(path, ref); own != "" {
return edit.SetJSON(path, edit.KV{Path: key, Value: own + "/" + ref})
}
if err := edit.SetJSON(path, edit.KV{Path: "provider." + magpieID, Value: magpieProviderJSON("opencode")}); err != nil {
if err := edit.SetJSON(path, edit.KV{Path: "provider." + magpieID, Value: provider()}); err != nil {
return err
}
}
@@ -307,23 +317,23 @@ func opencode(home, cfg string) *Agent {
}
}
return &Agent{
ID: "opencode", Name: "OpenCode", Icon: "opencode", Aliases: []string{"oc"},
UA: []string{"opencode"},
Bin: "opencode", Dir: dir, Path: path,
ID: id, Name: name, Icon: icon, Aliases: aliases,
UA: ua,
Bin: bin, Dir: dir, Path: path,
Check: func() string {
if !usesMagpie(get("model"), get("small_model")) {
return ""
}
return wiringOff("OpenCode", path, func(k string) (string, bool) { return edit.GetJSON(path, "provider."+magpieID+".options."+k) },
return wiringOff(name, path, func(k string) (string, bool) { return edit.GetJSON(path, "provider."+magpieID+".options."+k) },
"baseURL", gatewayV1(), "apiKey", gateway.Token)
},
Sync: func() error {
// a model of magpie's chosen, but its provider gone from the
// file: put it back, or OpenCode has nothing to send it to
// file: put it back, or the agent has nothing to send it to
if _, ok := edit.GetJSON(path, "provider."+magpieID); !ok && usesMagpie(get("model"), get("small_model")) {
return edit.SetJSON(path, edit.KV{Path: "provider." + magpieID, Value: magpieProviderJSON("opencode")})
return edit.SetJSON(path, edit.KV{Path: "provider." + magpieID, Value: provider()})
}
return syncJSON(path, "provider."+magpieID, func() any { return magpieProviderJSON("opencode") })
return syncJSON(path, "provider."+magpieID, provider)
},
Fields: []Field{
{Key: "model", Label: "model", Get: jsonGet(path, "model"), Set: set("model"), Options: opts("model")},
@@ -374,6 +384,18 @@ func sameGateway(base string) bool {
return base != "" && norm(base) == norm(gatewayV1())
}
func opencode(home, cfg string) *Agent {
return openCodeLike("opencode", "OpenCode", "opencode", "opencode",
filepath.Join(cfg, "opencode"), filepath.Join(home, ".local", "share", "opencode", "auth.json"),
[]string{"opencode"}, "oc")
}
func mimocode(home, cfg string) *Agent {
return openCodeLike("mimocode", "MiMo Code", "mimocode", "mimo",
filepath.Join(cfg, "mimocode"), filepath.Join(home, ".local", "share", "mimocode", "auth.json"),
[]string{"mimocode"}, "mimo")
}
func pi(home string) *Agent {
dir := filepath.Join(home, ".pi", "agent")
path := filepath.Join(dir, "settings.json")
+44
View File
@@ -12,6 +12,7 @@ import (
"github.com/yetone/magpie/internal/codexcat"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/settings"
)
// syncHome is a sandbox home with a models.dev catalog that knows glm-4.6's
@@ -235,3 +236,46 @@ func TestOpenCodeKeepsItsOwnGatewayProviders(t *testing.T) {
t.Fatal("magpie's provider not added")
}
}
// MiMo Code shares OpenCode's config shape: a sync writes magpie's provider
// into its own mimocode.json, and reads its own model list.
func TestMiMoCodeMirrorsOpenCode(t *testing.T) {
home := syncHome(t)
cfg := filepath.Join(home, ".config", "mimocode", "mimocode.json")
writeFile(t, cfg, `{"model":"magpie/relay/glm-4.6","provider":{"mine":{"name":"mine"}}}`)
if err := mimocode(home, filepath.Join(home, ".config")).Sync(); err != nil {
t.Fatal(err)
}
s := readFile(cfg)
if !strings.Contains(s, `"magpie"`) || !strings.Contains(s, `"mine"`) || !strings.Contains(s, `204800`) {
t.Fatalf("%s", s)
}
// one that doesn't use magpie gets nothing
body := `{"model":"mine/x","provider":{"mine":{"name":"mine"}}}`
writeFile(t, cfg, body)
if err := mimocode(home, filepath.Join(home, ".config")).Sync(); err != nil {
t.Fatal(err)
}
if s := readFile(cfg); s != body {
t.Fatalf("%s", s)
}
}
// magpieProviderJSONFor reads the catalog narrowed under the agent's own id,
// while the config shape stays OpenCode's: a visibility on mimocode narrows
// its provider block and leaves OpenCode's alone.
func TestMiMoCodeCatalogNarrowedSeparately(t *testing.T) {
syncHome(t)
if err := settings.Save(settings.Settings{Visible: map[string][]string{"mimocode": {"relay"}}}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { settings.Save(settings.Settings{}) })
mimo := magpieProviderJSONFor("opencode", "mimocode").(map[string]any)["models"].(map[string]any)
oc := magpieProviderJSON("opencode").(map[string]any)["models"].(map[string]any)
if len(mimo) != 1 || mimo["relay/glm-4.6"] == nil {
t.Fatalf("mimocode catalog: %v", mimo)
}
if oc["relay/glm-4.6"] == nil {
t.Fatalf("opencode catalog was narrowed too: %v", oc)
}
}
+1
View File
@@ -13,6 +13,7 @@ func TestAgentOf(t *testing.T) {
"codex_cli_rs/0.40.0 (Mac OS 26.0; arm64)": "codex",
"GeminiCLI/0.9.0 (darwin; arm64)": "gemini",
"opencode/1.2.3": "opencode",
"mimocode/1.2.3": "mimocode",
"curl/8.4.0": "curl",
"oh-my-pi/1.0": "omp",
"command-code/2.0": "commandcode",
+1 -1
View File
@@ -900,7 +900,7 @@ const I18N = {
"{agent} is on — it gets the shared instructions once there are some": "{agent} 已开启 — 有共享指令后就会写入",
"{agent} reads the shared instructions now": "{agent} 现在读取共享指令",
"{agent} reads {file} instead of this file while it has anything in it.": "只要 {file} 有内容,{agent} 就会读它而不是这个文件。",
"OpenCode reads Claude Code's CLAUDE.md when it has no AGENTS.md of its own.": "OpenCode 在没有自己的 AGENTS.md 时会读取 Claude Code 的 CLAUDE.md。",
"{agent} reads Claude Code's CLAUDE.md when it has no AGENTS.md of its own.": "{agent} 在没有自己的 AGENTS.md 时会读取 Claude Code 的 CLAUDE.md。",
"Only for {agent}, after the shared text": "仅给 {agent},接在共享内容之后",
"Anything only {agent} should be told": "只需告诉 {agent} 的内容",
"Switch {agent} on for it to read this.": "开启 {agent} 后它才会读到这些。",
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" height="1em" style="flex:none;line-height:1" viewBox="0 0 512 512" width="1em" xmlns="http://www.w3.org/2000/svg"><title>mimocode</title><path d="M320 224V352H192V224H320Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 359 B

+1 -1
View File
@@ -479,7 +479,7 @@
w.append(el("span", "", t("{agent} reads {file} instead of this file while it has anything in it.", { agent: a.name, file: tilde(a.override) }) + " "), pathLink(a.override));
det.append(w);
}
if (a.agent === "opencode") det.append(el("p", "lib-aside", t("OpenCode reads Claude Code's CLAUDE.md when it has no AGENTS.md of its own.")));
if (a.agent === "opencode" || a.agent === "mimocode") det.append(el("p", "lib-aside", t("{agent} reads Claude Code's CLAUDE.md when it has no AGENTS.md of its own.", { agent: a.name })));
const lab = el("label", "lib-lab", t("Only for {agent}, after the shared text", { agent: a.name }));
const ta = el("textarea", "lib-text small");
ta.dataset.lib = "extra:" + a.agent;
+1
View File
@@ -555,6 +555,7 @@
const HUES = {
claude: "#d97757", codex: "#6366f1", gemini: "#0ea5e9", copilot: "#a855f7", cursor: "#14b8a6", opencode: "#eab308",
crush: "#ec4899", goose: "#84cc16", pi: "#10b981", omp: "#f43f5e", dsh: "#06b6d4", commandcode: "#f97316",
mimocode: "#3b82f6",
};
const SPARE = ["#8b5cf6", "#22c55e", "#e11d48", "#0891b2", "#ca8a04", "#db2777", "#2563eb", "#65a30d"];
function hueOf(id) {
+2 -2
View File
@@ -34,7 +34,7 @@ func sandbox(t *testing.T) string {
}
for _, f := range []string{
".claude/settings.json", ".codex/config.toml", ".gemini/settings.json",
".config/opencode/opencode.json", ".pi/agent/settings.json", ".config/goose/config.yaml",
".config/opencode/opencode.json", ".config/mimocode/mimocode.json", ".pi/agent/settings.json", ".config/goose/config.yaml",
".cursor/cli-config.json", ".copilot/settings.json", ".config/crush/crush.json",
} {
write(t, filepath.Join(h, f), "")
@@ -86,7 +86,7 @@ func ids(ts []*Target) []string {
func TestTargets(t *testing.T) {
sandbox(t)
got := ids(Targets())
for _, id := range []string{"claude", "codex", "gemini", "opencode", "pi", "goose", "cursor", "copilot", "crush"} {
for _, id := range []string{"claude", "codex", "gemini", "opencode", "mimocode", "pi", "goose", "cursor", "copilot", "crush"} {
if !slices.Contains(got, id) {
t.Errorf("%s not a target: %v", id, got)
}
+8
View File
@@ -83,6 +83,14 @@ func targetOf(a *agent.Agent) *Target {
t.MCP = &mcpFile{Path: a.Path, Format: fmtOpenCode}
t.Skills = filepath.Join(d, "skills")
t.SkillsAlso = []string{"claude"}
case "mimocode":
// MiMo Code is OpenCode's config shape; it reads Claude Code's
// CLAUDE.md but its own skills only (a flag opens .claude's), so the
// shared skills go into its own folder, not Claude Code's.
d := filepath.Dir(a.Path)
t.Instructions = filepath.Join(d, "AGENTS.md")
t.MCP = &mcpFile{Path: a.Path, Format: fmtOpenCode}
t.Skills = filepath.Join(d, "skills")
case "pi":
d := os.Getenv("PI_CODING_AGENT_DIR")
if d == "" {
+1 -1
View File
@@ -74,7 +74,7 @@ const usage = `magpie — one place to pick every agent's model
magpie agents list every supported agent
magpie update [check] install the newest release (check: only say if there is one)
agents: claude (cc), codex, gemini, opencode (oc), pi, goose, cursor, copilot, crush
agents: claude (cc), codex, gemini, opencode (oc), mimocode, pi, goose, cursor, copilot, crush
`
var (
+2 -2
View File
@@ -115,7 +115,7 @@
<p>Keys live in <code>~/.config/magpie/providers.json</code>, readable by you alone. magpie never reads keys from your shell environment: what you add is what it uses.</p>
<h2 id="agents"><a class="anchor" href="#agents">2 · Pick a model for each agent</a></h2>
<p>The <span class="ui">Agents</span> tab has a row for each agent installed or configured on this machine. magpie knows Claude Code, Codex, Gemini CLI, OpenCode, Pi, Goose, Cursor, Copilot CLI, Crush, DeepSeek Harness, Command Code, omp, Devin, Hermes Agent, Grok Build, ZCode and Alma.</p>
<p>The <span class="ui">Agents</span> tab has a row for each agent installed or configured on this machine. magpie knows Claude Code, Codex, Gemini CLI, OpenCode, MiMo Code, Pi, Goose, Cursor, Copilot CLI, Crush, DeepSeek Harness, Command Code, omp, Devin, Hermes Agent, Grok Build, ZCode and Alma.</p>
<figure class="figure">
<img class="for-light" src="/img/agents-light.png" width="2080" height="1058" loading="lazy" alt="The Agents tab: one row per agent with the model it uses, and saved profiles at the bottom">
@@ -132,7 +132,7 @@
<tr><th>Agent</th><th>What a model through magpie writes</th></tr>
<tr><td>Claude Code</td><td><code>~/.claude/settings.json</code>: <code>ANTHROPIC_BASE_URL</code>, <code>ANTHROPIC_AUTH_TOKEN</code> and the model variables in its <code>env</code> block</td></tr>
<tr><td>Codex</td><td><code>~/.codex/config.toml</code>: a <code>[model_providers.magpie]</code> table and the model, with the list in <code>magpie-models.json</code>. Your ChatGPT sign-in is left alone</td></tr>
<tr><td>OpenCode, Pi, Crush</td><td>A <code>magpie</code> provider entry, and <code>magpie/provider/model</code></td></tr>
<tr><td>OpenCode, MiMo Code, Pi, Crush</td><td>A <code>magpie</code> provider entry, and <code>magpie/provider/model</code></td></tr>
<tr><td>Gemini CLI</td><td><code>GOOGLE_GEMINI_BASE_URL</code> pointed at the gateway, API-key auth, and the model</td></tr>
</table></div>
<p><b>A running session keeps the model it started with.</b> Agents read their config when they start, so the change applies to the next session. Codex builds its model list at start-up too: restart the Codex app and open <code>codex</code> sessions after a switch.</p>
+2 -2
View File
@@ -115,7 +115,7 @@
<p>密钥保存在 <code>~/.config/magpie/providers.json</code>,只有你自己可读。magpie 从不读取 shell 环境变量里的密钥:你添加什么,它就用什么。</p>
<h2 id="agents"><a class="anchor" href="#agents">2 · 给每个 Agent 选模型</a></h2>
<p><span class="ui">Agent</span> 标签页里,本机装了或配置过的每个 Agent 各占一行。magpie 支持 Claude Code、Codex、Gemini CLI、OpenCode、Pi、Goose、Cursor、Copilot CLI、Crush、DeepSeek Harness、Command Code、omp、Devin、Hermes Agent、Grok Build、ZCode 和 Alma。</p>
<p><span class="ui">Agent</span> 标签页里,本机装了或配置过的每个 Agent 各占一行。magpie 支持 Claude Code、Codex、Gemini CLI、OpenCode、MiMo Code、Pi、Goose、Cursor、Copilot CLI、Crush、DeepSeek Harness、Command Code、omp、Devin、Hermes Agent、Grok Build、ZCode 和 Alma。</p>
<figure class="figure">
<img class="for-light" src="/img/agents-zh-light.png" width="2080" height="1058" loading="lazy" alt="Agent 页:每个 Agent 一行,显示当前模型,底部是保存的方案">
@@ -132,7 +132,7 @@
<tr><th>Agent</th><th>通过 magpie 选模型时写入什么</th></tr>
<tr><td>Claude Code</td><td><code>~/.claude/settings.json</code>:<code>env</code> 里的 <code>ANTHROPIC_BASE_URL</code>、<code>ANTHROPIC_AUTH_TOKEN</code> 和模型变量</td></tr>
<tr><td>Codex</td><td><code>~/.codex/config.toml</code>:一个 <code>[model_providers.magpie]</code> 表和模型,模型列表写在 <code>magpie-models.json</code>。你的 ChatGPT 登录不受影响</td></tr>
<tr><td>OpenCode、Pi、Crush</td><td>一个 <code>magpie</code> 供应商条目,以及 <code>magpie/provider/model</code></td></tr>
<tr><td>OpenCode、MiMo Code、Pi、Crush</td><td>一个 <code>magpie</code> 供应商条目,以及 <code>magpie/provider/model</code></td></tr>
<tr><td>Gemini CLI</td><td><code>GOOGLE_GEMINI_BASE_URL</code> 指向网关、API 密钥认证,以及模型</td></tr>
</table></div>
<p><b>已在运行的会话保持它启动时的模型。</b>Agent 启动时读取配置,所以改动从下一个会话生效。Codex 也在启动时生成模型列表:切换后请重启 Codex 应用和已打开的 <code>codex</code> 会话。</p>
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" height="1em" style="flex:none;line-height:1" viewBox="0 0 512 512" width="1em" xmlns="http://www.w3.org/2000/svg"><title>mimocode</title><path d="M320 224V352H192V224H320Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 359 B

+1
View File
@@ -396,6 +396,7 @@ footer a:hover { color: var(--ink); }
<li><img src="/icons/codex-color.svg" alt="">Codex</li>
<li><img src="/icons/geminicli-color.svg" alt="">Gemini CLI</li>
<li><img class="bird" src="/icons/opencode.svg" alt="">OpenCode</li>
<li><img class="bird" src="/icons/mimocode.svg" alt="">MiMo Code</li>
<li><img class="bird" src="/icons/pi.svg" alt="">Pi</li>
<li><img class="bird" src="/icons/goose.svg" alt="">Goose</li>
<li><img class="bird" src="/icons/cursor.svg" alt="">Cursor CLI</li>