feat(adapter): raise on third-party model without ANTHROPIC_BASE_URL
Aligns setup()'s third-party-model-without-URL handling with create_executor()'s pre-validate (#19) — both unrecoverable misconfigurations now raise ValueError at boot instead of one warning and one raising. Why: a third-party (mimo-*) model selected without ANTHROPIC_BASE_URL sends every LLM request to api.anthropic.com with a non-Anthropic key, 401-ing every prompt. Workspace boots, looks "online" via heartbeat, but is structurally broken on the user-facing path. The previous warning-only path produced the same end-user symptom as the 2026-04-30 incident (workspace looks alive, every interaction fails) just via a different misconfig shape. Symmetry: create_executor raises when ANTHROPIC_BASE_URL is set to a non-Anthropic host but no model is picked. setup() now raises when a third-party model is picked but no URL is set. Together they catch both halves of the misconfig surface at boot, before the workspace enters "online" status. Adds 4 setup() tests: - raises on third-party + no URL - passes on third-party + URL - passes on OAuth alias (sonnet) + no URL - passes on Anthropic API id (claude-*) + no URL Stubs molecule_runtime.plugins.load_plugins as a no-op so the pass-path tests run cleanly without the runtime installed. Test count: 11 (7 create_executor + 4 setup). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3d83e0513c
commit
c646b8cebe
23
adapter.py
23
adapter.py
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user