adapter: warn for the right env var per auth mode + log boot banner

The pre-multi-provider warning hardcoded CLAUDE_CODE_OAUTH_TOKEN — it
fired even when an operator legitimately picked claude-sonnet-4-6 (API
key) or mimo-v2-flash (third-party) and set ANTHROPIC_API_KEY instead.
Misleading.

Now classifies the picked model into oauth / anthropic_api /
third_party_anthropic_compat and warns about the env var that auth path
actually needs. Adds a single-line boot banner so workspace logs surface
which provider was selected and (for third-party) which base-URL host
took effect — host-only, never full URL.

Adds an additional warning when a third-party model is selected but
ANTHROPIC_BASE_URL is unset, since the symptom otherwise is silent
fall-through to api.anthropic.com with a third-party key (401).

Functional tests against 14 model-id cases (oauth aliases, claude-*
versioned, all 4 mimo-* variants, case-insensitivity, empty/None,
unknown id fallback) all pass — see commit's pre-push validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-29 03:15:21 -07:00
parent a21d16d94f
commit 824bc4a176

View File

@ -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(