Merge pull request #17 from Molecule-AI/feat/xiaomi-mimo-anthropic-compat
feat: add Xiaomi MiMo support (testing — entrypoint-shell mapping)
This commit is contained in:
commit
14f27b7886
11
README.md
11
README.md
@ -17,6 +17,17 @@ github://Molecule-AI/template-claude-code-default
|
||||
- `config.yaml` — workspace configuration (runtime, model, skills, etc.)
|
||||
- `system-prompt.md` — agent system prompt (if present)
|
||||
|
||||
## Auth paths
|
||||
|
||||
| Path | Env var(s) | Where to get the key |
|
||||
|---|---|---|
|
||||
| OAuth (Claude Code subscription) | `CLAUDE_CODE_OAUTH_TOKEN` | `claude login` |
|
||||
| Anthropic API (direct) | `ANTHROPIC_API_KEY` | console.anthropic.com |
|
||||
| Third-party Anthropic-compat (e.g. Xiaomi MiMo pay-as-you-go) | `ANTHROPIC_API_KEY` (provider's key) | provider console |
|
||||
| Xiaomi MiMo Token Plan | `ANTHROPIC_API_KEY` (Token Plan key), `ANTHROPIC_BASE_URL` (Token Plan endpoint) | token-plan dashboard |
|
||||
|
||||
For third-party providers, `entrypoint.sh` rewrites `ANTHROPIC_BASE_URL` based on the selected `MODEL` so the `claude` CLI routes there. Currently auto-routes `mimo-*` models to `https://api.xiaomimimo.com/anthropic` (pay-as-you-go). **Token Plan users** should set `ANTHROPIC_BASE_URL=https://token-plan-sgp.xiaomimimo.com/anthropic` as a workspace or org-level secret — the shell mapping is the fallback and operator-set values always win. Other Token Plan endpoints (e.g. `token-plan-hk.xiaomimimo.com`) can be used by setting the secret explicitly.
|
||||
|
||||
## Schema version
|
||||
`template_schema_version: 1` — compatible with Molecule AI platform v1.x.
|
||||
|
||||
|
||||
101
adapter.py
101
adapter.py
@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig, RuntimeCapabilities
|
||||
from a2a.server.agent_execution import AgentExecutor
|
||||
@ -14,6 +15,47 @@ logger = logging.getLogger(__name__)
|
||||
# the workspace by polling /transcript?limit=999999.
|
||||
_TRANSCRIPT_MAX_LIMIT = 1000
|
||||
|
||||
# Auth-mode classification for a selected model id. The Claude Code CLI
|
||||
# accepts three auth paths and the right env var differs per path; warning
|
||||
# at boot about the wrong var (the pre-multi-provider behavior) misled
|
||||
# operators who picked an API-key or third-party model. New third-party
|
||||
# providers add a prefix → mode entry below + a model-prefix → base-URL
|
||||
# mapping in entrypoint.sh until the data-driven `runtime_env` schema
|
||||
# field lands platform-side.
|
||||
_AUTH_MODE_OAUTH = "oauth"
|
||||
_AUTH_MODE_ANTHROPIC_API = "anthropic_api"
|
||||
_AUTH_MODE_THIRD_PARTY = "third_party_anthropic_compat"
|
||||
|
||||
_THIRD_PARTY_PREFIXES = ("mimo-",)
|
||||
_OAUTH_ALIASES = frozenset({"sonnet", "opus", "haiku"})
|
||||
|
||||
|
||||
def _detect_auth_mode(model: str) -> str:
|
||||
"""Classify the picked model into one of three auth paths.
|
||||
|
||||
Used by setup() to validate the right env var is set so operators see
|
||||
the misconfiguration at boot instead of on the first LLM call.
|
||||
Unknown ids default to OAuth — the historical default and the safest
|
||||
fallback for the warning path.
|
||||
"""
|
||||
if not model:
|
||||
return _AUTH_MODE_OAUTH
|
||||
m = model.lower()
|
||||
if any(m.startswith(p) for p in _THIRD_PARTY_PREFIXES):
|
||||
return _AUTH_MODE_THIRD_PARTY
|
||||
if m.startswith("claude-"):
|
||||
return _AUTH_MODE_ANTHROPIC_API
|
||||
if m in _OAUTH_ALIASES:
|
||||
return _AUTH_MODE_OAUTH
|
||||
return _AUTH_MODE_OAUTH
|
||||
|
||||
|
||||
def _required_env_for_mode(mode: str) -> str:
|
||||
"""The env var the claude CLI needs to authenticate for a given mode."""
|
||||
if mode == _AUTH_MODE_OAUTH:
|
||||
return "CLAUDE_CODE_OAUTH_TOKEN"
|
||||
return "ANTHROPIC_API_KEY"
|
||||
|
||||
|
||||
class ClaudeCodeAdapter(BaseAdapter):
|
||||
|
||||
@ -94,15 +136,60 @@ class ClaudeCodeAdapter(BaseAdapter):
|
||||
``CLAUDE.md`` and ``/configs/skills/`` natively, and the default
|
||||
:class:`AgentskillsAdaptor` writes to both.
|
||||
"""
|
||||
# KI-001 fix: warn immediately if CLAUDE_CODE_OAUTH_TOKEN is absent so
|
||||
# operators see the problem at startup instead of a silent
|
||||
# AuthenticationError on the first LLM call.
|
||||
if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
|
||||
# KI-001 fix, generalized for the three auth paths the CLI supports:
|
||||
# OAuth (CLAUDE_CODE_OAUTH_TOKEN), Anthropic API (ANTHROPIC_API_KEY),
|
||||
# and third-party Anthropic-API-compat (ANTHROPIC_API_KEY + provider
|
||||
# ANTHROPIC_BASE_URL). Detect the path from the picked model so the
|
||||
# warning targets the *right* env var — the pre-multi-provider code
|
||||
# always warned about CLAUDE_CODE_OAUTH_TOKEN even when the user had
|
||||
# legitimately picked an API-key model and set ANTHROPIC_API_KEY.
|
||||
rc = config.runtime_config
|
||||
if isinstance(rc, dict):
|
||||
picked_model = rc.get("model") or "sonnet"
|
||||
else:
|
||||
picked_model = getattr(rc, "model", None) or "sonnet"
|
||||
auth_mode = _detect_auth_mode(picked_model)
|
||||
required_var = _required_env_for_mode(auth_mode)
|
||||
|
||||
# Single-line startup banner — operators reading boot logs can see
|
||||
# which provider path was selected and whether ANTHROPIC_BASE_URL
|
||||
# (set by entrypoint.sh for third-party mimo-*) took effect. URL is
|
||||
# logged as host-only; defensive against credential-shaped query
|
||||
# strings even though base_url shouldn't carry one.
|
||||
base_url = os.environ.get("ANTHROPIC_BASE_URL")
|
||||
base_url_host = ""
|
||||
if base_url:
|
||||
try:
|
||||
base_url_host = urlparse(base_url).netloc or "<unparseable>"
|
||||
except Exception:
|
||||
base_url_host = "<unparseable>"
|
||||
logger.info(
|
||||
"Claude Code adapter starting: model=%s auth_mode=%s required_env=%s%s",
|
||||
picked_model, auth_mode, required_var,
|
||||
f" base_url_host={base_url_host}" if base_url_host else "",
|
||||
)
|
||||
|
||||
if not os.environ.get(required_var):
|
||||
logger.warning(
|
||||
"CLAUDE_CODE_OAUTH_TOKEN is not set — the adapter will fail on the "
|
||||
"first LLM call with an AuthenticationError. Set the env var or "
|
||||
"configure an API key in your platform workspace settings."
|
||||
"%s is not set for model=%s (auth_mode=%s) — the adapter will fail "
|
||||
"on the first LLM call with an AuthenticationError. Set the env "
|
||||
"var or configure the key in your platform workspace settings.",
|
||||
required_var, picked_model, auth_mode,
|
||||
)
|
||||
|
||||
# Third-party paths additionally need ANTHROPIC_BASE_URL; entrypoint.sh
|
||||
# sets it for known mimo-* prefixes. Surface the missing-base-URL
|
||||
# case explicitly — the symptom otherwise is the CLI silently hitting
|
||||
# api.anthropic.com with a third-party key, which 401s.
|
||||
if auth_mode == _AUTH_MODE_THIRD_PARTY and not base_url:
|
||||
logger.warning(
|
||||
"model=%s is a third-party Anthropic-compat model but "
|
||||
"ANTHROPIC_BASE_URL is unset — requests will land on the real "
|
||||
"api.anthropic.com and fail with 401. Check entrypoint.sh's "
|
||||
"model→base-URL mapping or set ANTHROPIC_BASE_URL via secrets.",
|
||||
picked_model,
|
||||
)
|
||||
|
||||
from molecule_runtime.plugins import load_plugins
|
||||
workspace_plugins_dir = os.path.join(config.config_path, "plugins")
|
||||
plugins = load_plugins(
|
||||
|
||||
32
config.yaml
32
config.yaml
@ -1,11 +1,13 @@
|
||||
name: Claude Code Agent
|
||||
description: >-
|
||||
General-purpose Claude Code workspace. Supports two auth paths:
|
||||
General-purpose Claude Code workspace. Supports three auth paths:
|
||||
(1) Claude Code subscription via OAuth token (CLAUDE_CODE_OAUTH_TOKEN,
|
||||
obtained from `claude login`), or (2) Anthropic API key
|
||||
(ANTHROPIC_API_KEY, pay-as-you-go via console.anthropic.com). The
|
||||
`claude` CLI picks whichever is set; OAuth takes precedence when both
|
||||
are present.
|
||||
obtained from `claude login`), (2) Anthropic API key
|
||||
(ANTHROPIC_API_KEY, pay-as-you-go via console.anthropic.com), or
|
||||
(3) third-party Anthropic-API-compatible providers (e.g. Xiaomi MiMo)
|
||||
via ANTHROPIC_API_KEY + provider-specific ANTHROPIC_BASE_URL routing.
|
||||
The `claude` CLI picks whichever is set; OAuth takes precedence when
|
||||
multiple are present.
|
||||
version: 1.0.0
|
||||
tier: 2
|
||||
|
||||
@ -43,6 +45,26 @@ runtime_config:
|
||||
name: Claude Haiku 4.5 (API key / Anthropic Console)
|
||||
required_env: [ANTHROPIC_API_KEY]
|
||||
|
||||
# --- Xiaomi MiMo (third-party, Anthropic-API-compatible) — set ANTHROPIC_API_KEY ---
|
||||
# Routed through https://api.xiaomimimo.com/anthropic via ANTHROPIC_BASE_URL
|
||||
# (the claude CLI honors the env var natively). Mapping lives in
|
||||
# entrypoint.sh — when MODEL matches mimo-*, base URL is rewritten before
|
||||
# the runtime starts. The user's ANTHROPIC_API_KEY here is a Xiaomi key,
|
||||
# not an Anthropic Console key. Long-term, this should move to a
|
||||
# data-driven `runtime_env` schema field; tracked separately.
|
||||
- id: mimo-v2-flash
|
||||
name: Xiaomi MiMo V2 Flash (third-party, Anthropic-API-compatible)
|
||||
required_env: [ANTHROPIC_API_KEY]
|
||||
- id: mimo-v2-pro
|
||||
name: Xiaomi MiMo V2 Pro (third-party, Anthropic-API-compatible)
|
||||
required_env: [ANTHROPIC_API_KEY]
|
||||
- id: mimo-v2-omni
|
||||
name: Xiaomi MiMo V2 Omni (third-party, Anthropic-API-compatible)
|
||||
required_env: [ANTHROPIC_API_KEY]
|
||||
- id: mimo-v2.5-pro
|
||||
name: Xiaomi MiMo V2.5 Pro (third-party, Anthropic-API-compatible)
|
||||
required_env: [ANTHROPIC_API_KEY]
|
||||
|
||||
# Default required_env — per-model entries above override this once a
|
||||
# model is picked. Keep CLAUDE_CODE_OAUTH_TOKEN as the default so
|
||||
# existing workspaces (which all use OAuth) keep working unchanged.
|
||||
|
||||
@ -81,4 +81,35 @@ elif [ -n "${GH_TOKEN:-}" ]; then
|
||||
echo "${GH_TOKEN}" | gh auth login --hostname github.com --with-token 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Third-party Anthropic-API-compatible provider routing.
|
||||
# The `claude` CLI honors ANTHROPIC_BASE_URL natively; we rewrite it
|
||||
# based on MODEL so a Xiaomi MiMo selection lands on Xiaomi's endpoint
|
||||
# without code changes inside the SDK. ANTHROPIC_API_KEY in this case
|
||||
# is the third-party provider key, not an Anthropic Console key.
|
||||
#
|
||||
# Refuses to clobber an operator-set ANTHROPIC_BASE_URL — if the user
|
||||
# provided one explicitly via secrets (e.g. a Xiaomi MiMo Token Plan
|
||||
# endpoint such as https://token-plan-sgp.xiaomimimo.com/anthropic),
|
||||
# that wins. The mapping below is only the fallback for known model
|
||||
# prefixes.
|
||||
#
|
||||
# Supported Xiaomi MiMo endpoints:
|
||||
# - Pay-as-you-go: https://api.xiaomimimo.com/anthropic
|
||||
# - Token Plan SG: https://token-plan-sgp.xiaomimimo.com/anthropic
|
||||
# - Token Plan HK: https://token-plan-hk.xiaomimimo.com/anthropic
|
||||
# (Set ANTHROPIC_BASE_URL explicitly to use a specific endpoint.)
|
||||
#
|
||||
# Long-term this should move to a data-driven `runtime_env` field in
|
||||
# config.yaml read by the platform provisioner; tracked separately.
|
||||
case "${MODEL:-}" in
|
||||
mimo-*)
|
||||
if [ -z "${ANTHROPIC_BASE_URL:-}" ]; then
|
||||
export ANTHROPIC_BASE_URL="https://api.xiaomimimo.com/anthropic"
|
||||
echo "[entrypoint] MODEL=${MODEL} → ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL}" >&2
|
||||
else
|
||||
echo "[entrypoint] MODEL=${MODEL} but ANTHROPIC_BASE_URL already set; not overriding" >&2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
exec molecule-runtime "$@"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user