Merge pull request #21 from Molecule-AI/feat/setup-raise-on-third-party-no-base-url

feat(adapter): raise on third-party model without ANTHROPIC_BASE_URL
This commit is contained in:
Hongming Wang 2026-04-30 23:09:41 -07:00 committed by GitHub
commit e02c5bf34b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 100 additions and 9 deletions

View File

@ -178,16 +178,21 @@ class ClaudeCodeAdapter(BaseAdapter):
)
# 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.
# sets it for known mimo-* prefixes. Fail fast on the missing-base-URL
# combo — the symptom otherwise is the CLI silently hitting
# api.anthropic.com with a non-Anthropic key, every LLM call 401s, and
# the workspace looks "online" while being structurally broken.
# Symmetric with create_executor's pre-validate raise on the inverse
# combo (URL set, no model picked) — both unrecoverable misconfigs
# that would put the workspace into a "boots but never works" state.
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,
raise ValueError(
f"claude-code adapter: model={picked_model} is a third-party "
"Anthropic-compat model but ANTHROPIC_BASE_URL is unset. "
"Without it, requests land on api.anthropic.com with a "
"non-Anthropic key and 401 every call. Fix: check "
"entrypoint.sh's model→base-URL mapping for this model "
"prefix, or set ANTHROPIC_BASE_URL as a workspace secret."
)
from molecule_runtime.plugins import load_plugins

View File

@ -57,9 +57,15 @@ def _install_stubs():
mr.adapters.base.BaseAdapter = _StubBaseAdapter
mr.adapters.base.AdapterConfig = _StubAdapterConfig
mr.adapters.base.RuntimeCapabilities = _StubRuntimeCapabilities
# adapter.setup() lazy-imports molecule_runtime.plugins.load_plugins.
# Stub it as a no-op returning [] so setup() pass-paths run cleanly
# without needing the real runtime installed in the test env.
mr.plugins = types.ModuleType("molecule_runtime.plugins")
mr.plugins.load_plugins = lambda **_kwargs: []
sys.modules["molecule_runtime"] = mr
sys.modules["molecule_runtime.adapters"] = mr.adapters
sys.modules["molecule_runtime.adapters.base"] = mr.adapters.base
sys.modules["molecule_runtime.plugins"] = mr.plugins
if "a2a" not in sys.modules:
a2a = types.ModuleType("a2a")
a2a.server = types.ModuleType("a2a.server")
@ -222,3 +228,83 @@ async def test_create_executor_passes_when_unparseable_url(adapter, monkeypatch)
# fallback. The SDK will fail; that's not the adapter's job.
executor = await adapter.create_executor(cfg)
assert executor is not None
# ---- setup() pre-validation tests ----
#
# Symmetric to create_executor's pre-validate: setup() raises on the
# inverse misconfig (third-party MODEL picked but ANTHROPIC_BASE_URL
# unset). Both produce "boots but every LLM call fails" if not caught;
# raising at boot keeps the workspace from entering "online" status with
# structurally-broken auth.
@pytest.mark.asyncio
async def test_setup_raises_when_third_party_model_and_no_base_url(
adapter, monkeypatch
):
"""mimo-* model picked but no ANTHROPIC_BASE_URL → raise.
Without the URL, every LLM request lands on api.anthropic.com with
a non-Anthropic key and 401s. The adapter should fail at boot
rather than ship a workspace that 401s on every prompt.
"""
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
cfg = _StubAdapterConfig(
runtime_config={"model": "mimo-v2-flash"}, config_path="/tmp/configs"
)
with pytest.raises(ValueError) as exc_info:
await adapter.setup(cfg)
msg = str(exc_info.value)
assert "mimo-v2-flash" in msg
assert "ANTHROPIC_BASE_URL" in msg
@pytest.mark.asyncio
async def test_setup_passes_when_third_party_model_with_base_url(
adapter, monkeypatch
):
"""The fix path: third-party model + base URL set → setup() runs
cleanly through to plugin install (which is a no-op stub here).
"""
monkeypatch.setenv(
"ANTHROPIC_BASE_URL", "https://api.xiaomimimo.com/anthropic"
)
cfg = _StubAdapterConfig(
runtime_config={"model": "mimo-v2-flash"}, config_path="/tmp/configs"
)
# Should complete without raising. Plugin install is stubbed.
await adapter.setup(cfg)
@pytest.mark.asyncio
async def test_setup_passes_when_oauth_model_no_base_url(adapter, monkeypatch):
"""OAuth-aliased models (sonnet/opus/haiku) are Anthropic-native; no
base URL is required. setup() must not raise on the OAuth path even
though base_url is unset that's the historical happy path.
"""
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
cfg = _StubAdapterConfig(
runtime_config={"model": "sonnet"}, config_path="/tmp/configs"
)
await adapter.setup(cfg)
@pytest.mark.asyncio
async def test_setup_passes_when_anthropic_api_model_no_base_url(
adapter, monkeypatch
):
"""claude-* versioned ids are Anthropic API-key path; base URL
optional (defaults to api.anthropic.com). setup() must not raise.
"""
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
cfg = _StubAdapterConfig(
runtime_config={"model": "claude-sonnet-4-6"},
config_path="/tmp/configs",
)
await adapter.setup(cfg)