fix: Alibaba/DashScope: preserve model dots, fix 401 auth, fix dead provider check (salvage #1748 + fix #2314)
fix: Alibaba/DashScope: preserve model dots, fix 401 auth, fix dead provider check (salvage #1748 + fix #2314)
This commit is contained in:
commit
b73d221324
3 changed files with 40 additions and 10 deletions
|
|
@ -656,19 +656,21 @@ def refresh_hermes_oauth_token() -> Optional[str]:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def normalize_model_name(model: str) -> str:
|
def normalize_model_name(model: str, preserve_dots: bool = False) -> str:
|
||||||
"""Normalize a model name for the Anthropic API.
|
"""Normalize a model name for the Anthropic API.
|
||||||
|
|
||||||
- Strips 'anthropic/' prefix (OpenRouter format, case-insensitive)
|
- Strips 'anthropic/' prefix (OpenRouter format, case-insensitive)
|
||||||
- Converts dots to hyphens in version numbers (OpenRouter uses dots,
|
- Converts dots to hyphens in version numbers (OpenRouter uses dots,
|
||||||
Anthropic uses hyphens: claude-opus-4.6 → claude-opus-4-6)
|
Anthropic uses hyphens: claude-opus-4.6 → claude-opus-4-6), unless
|
||||||
|
preserve_dots is True (e.g. for Alibaba/DashScope: qwen3.5-plus).
|
||||||
"""
|
"""
|
||||||
lower = model.lower()
|
lower = model.lower()
|
||||||
if lower.startswith("anthropic/"):
|
if lower.startswith("anthropic/"):
|
||||||
model = model[len("anthropic/"):]
|
model = model[len("anthropic/"):]
|
||||||
# OpenRouter uses dots for version separators (claude-opus-4.6),
|
if not preserve_dots:
|
||||||
# Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens.
|
# OpenRouter uses dots for version separators (claude-opus-4.6),
|
||||||
model = model.replace(".", "-")
|
# Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens.
|
||||||
|
model = model.replace(".", "-")
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1006,16 +1008,20 @@ def build_anthropic_kwargs(
|
||||||
reasoning_config: Optional[Dict[str, Any]],
|
reasoning_config: Optional[Dict[str, Any]],
|
||||||
tool_choice: Optional[str] = None,
|
tool_choice: Optional[str] = None,
|
||||||
is_oauth: bool = False,
|
is_oauth: bool = False,
|
||||||
|
preserve_dots: bool = False,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Build kwargs for anthropic.messages.create().
|
"""Build kwargs for anthropic.messages.create().
|
||||||
|
|
||||||
When *is_oauth* is True, applies Claude Code compatibility transforms:
|
When *is_oauth* is True, applies Claude Code compatibility transforms:
|
||||||
system prompt prefix, tool name prefixing, and prompt sanitization.
|
system prompt prefix, tool name prefixing, and prompt sanitization.
|
||||||
|
|
||||||
|
When *preserve_dots* is True, model name dots are not converted to hyphens
|
||||||
|
(for Alibaba/DashScope anthropic-compatible endpoints: qwen3.5-plus).
|
||||||
"""
|
"""
|
||||||
system, anthropic_messages = convert_messages_to_anthropic(messages)
|
system, anthropic_messages = convert_messages_to_anthropic(messages)
|
||||||
anthropic_tools = convert_tools_to_anthropic(tools) if tools else []
|
anthropic_tools = convert_tools_to_anthropic(tools) if tools else []
|
||||||
|
|
||||||
model = normalize_model_name(model)
|
model = normalize_model_name(model, preserve_dots=preserve_dots)
|
||||||
effective_max_tokens = max_tokens or 16384
|
effective_max_tokens = max_tokens or 16384
|
||||||
|
|
||||||
# ── OAuth: Claude Code identity ──────────────────────────────────
|
# ── OAuth: Claude Code identity ──────────────────────────────────
|
||||||
|
|
|
||||||
26
run_agent.py
26
run_agent.py
|
|
@ -681,7 +681,10 @@ class AIAgent:
|
||||||
|
|
||||||
if self.api_mode == "anthropic_messages":
|
if self.api_mode == "anthropic_messages":
|
||||||
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
|
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
|
||||||
effective_key = api_key or resolve_anthropic_token() or ""
|
# Alibaba/DashScope use their own API key; do not fall back to ANTHROPIC_TOKEN (Fixes #1739 401).
|
||||||
|
_base = (base_url or "").lower()
|
||||||
|
_is_alibaba_dashscope = (self.provider == "alibaba") or ("dashscope" in _base) or ("aliyuncs" in _base)
|
||||||
|
effective_key = (api_key or "") if _is_alibaba_dashscope else (api_key or resolve_anthropic_token() or "")
|
||||||
self.api_key = effective_key
|
self.api_key = effective_key
|
||||||
self._anthropic_api_key = effective_key
|
self._anthropic_api_key = effective_key
|
||||||
self._anthropic_base_url = base_url
|
self._anthropic_base_url = base_url
|
||||||
|
|
@ -2333,7 +2336,7 @@ class AIAgent:
|
||||||
# Alibaba Coding Plan API always returns "glm-4.7" as model name regardless
|
# Alibaba Coding Plan API always returns "glm-4.7" as model name regardless
|
||||||
# of the requested model. Inject explicit model identity into the system prompt
|
# of the requested model. Inject explicit model identity into the system prompt
|
||||||
# so the agent can correctly report which model it is (workaround for API bug).
|
# so the agent can correctly report which model it is (workaround for API bug).
|
||||||
if self.provider in ("alibaba-coding-plan", "alibaba-coding-plan-anthropic"):
|
if self.provider == "alibaba":
|
||||||
_model_short = self.model.split("/")[-1] if "/" in self.model else self.model
|
_model_short = self.model.split("/")[-1] if "/" in self.model else self.model
|
||||||
prompt_parts.append(
|
prompt_parts.append(
|
||||||
f"You are powered by the model named {_model_short}. "
|
f"You are powered by the model named {_model_short}. "
|
||||||
|
|
@ -3337,6 +3340,10 @@ class AIAgent:
|
||||||
def _try_refresh_anthropic_client_credentials(self) -> bool:
|
def _try_refresh_anthropic_client_credentials(self) -> bool:
|
||||||
if self.api_mode != "anthropic_messages" or not hasattr(self, "_anthropic_api_key"):
|
if self.api_mode != "anthropic_messages" or not hasattr(self, "_anthropic_api_key"):
|
||||||
return False
|
return False
|
||||||
|
# Alibaba/DashScope use their own API key; do not refresh from ANTHROPIC_TOKEN (Fixes #1739 401).
|
||||||
|
_base = (getattr(self, "_anthropic_base_url", None) or "").lower()
|
||||||
|
if (self.provider == "alibaba") or ("dashscope" in _base) or ("aliyuncs" in _base):
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from agent.anthropic_adapter import resolve_anthropic_token, build_anthropic_client
|
from agent.anthropic_adapter import resolve_anthropic_token, build_anthropic_client
|
||||||
|
|
@ -3940,6 +3947,13 @@ class AIAgent:
|
||||||
)
|
)
|
||||||
return transformed
|
return transformed
|
||||||
|
|
||||||
|
def _anthropic_preserve_dots(self) -> bool:
|
||||||
|
"""True when using Alibaba/DashScope anthropic-compatible endpoint (model names keep dots, e.g. qwen3.5-plus)."""
|
||||||
|
if (getattr(self, "provider", "") or "").lower() == "alibaba":
|
||||||
|
return True
|
||||||
|
base = (getattr(self, "base_url", "") or "").lower()
|
||||||
|
return "dashscope" in base or "aliyuncs" in base
|
||||||
|
|
||||||
def _build_api_kwargs(self, api_messages: list) -> dict:
|
def _build_api_kwargs(self, api_messages: list) -> dict:
|
||||||
"""Build the keyword arguments dict for the active API mode."""
|
"""Build the keyword arguments dict for the active API mode."""
|
||||||
if self.api_mode == "anthropic_messages":
|
if self.api_mode == "anthropic_messages":
|
||||||
|
|
@ -3952,6 +3966,7 @@ class AIAgent:
|
||||||
max_tokens=self.max_tokens,
|
max_tokens=self.max_tokens,
|
||||||
reasoning_config=self.reasoning_config,
|
reasoning_config=self.reasoning_config,
|
||||||
is_oauth=getattr(self, "_is_anthropic_oauth", False),
|
is_oauth=getattr(self, "_is_anthropic_oauth", False),
|
||||||
|
preserve_dots=self._anthropic_preserve_dots(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.api_mode == "codex_responses":
|
if self.api_mode == "codex_responses":
|
||||||
|
|
@ -4413,6 +4428,7 @@ class AIAgent:
|
||||||
model=self.model, messages=api_messages,
|
model=self.model, messages=api_messages,
|
||||||
tools=[memory_tool_def], max_tokens=5120,
|
tools=[memory_tool_def], max_tokens=5120,
|
||||||
reasoning_config=None,
|
reasoning_config=None,
|
||||||
|
preserve_dots=self._anthropic_preserve_dots(),
|
||||||
)
|
)
|
||||||
response = self._anthropic_messages_create(ant_kwargs)
|
response = self._anthropic_messages_create(ant_kwargs)
|
||||||
elif not _aux_available:
|
elif not _aux_available:
|
||||||
|
|
@ -5221,7 +5237,8 @@ class AIAgent:
|
||||||
from agent.anthropic_adapter import build_anthropic_kwargs as _bak, normalize_anthropic_response as _nar
|
from agent.anthropic_adapter import build_anthropic_kwargs as _bak, normalize_anthropic_response as _nar
|
||||||
_ant_kw = _bak(model=self.model, messages=api_messages, tools=None,
|
_ant_kw = _bak(model=self.model, messages=api_messages, tools=None,
|
||||||
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config,
|
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config,
|
||||||
is_oauth=getattr(self, '_is_anthropic_oauth', False))
|
is_oauth=getattr(self, '_is_anthropic_oauth', False),
|
||||||
|
preserve_dots=self._anthropic_preserve_dots())
|
||||||
summary_response = self._anthropic_messages_create(_ant_kw)
|
summary_response = self._anthropic_messages_create(_ant_kw)
|
||||||
_msg, _ = _nar(summary_response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
|
_msg, _ = _nar(summary_response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
|
||||||
final_response = (_msg.content or "").strip()
|
final_response = (_msg.content or "").strip()
|
||||||
|
|
@ -5252,7 +5269,8 @@ class AIAgent:
|
||||||
from agent.anthropic_adapter import build_anthropic_kwargs as _bak2, normalize_anthropic_response as _nar2
|
from agent.anthropic_adapter import build_anthropic_kwargs as _bak2, normalize_anthropic_response as _nar2
|
||||||
_ant_kw2 = _bak2(model=self.model, messages=api_messages, tools=None,
|
_ant_kw2 = _bak2(model=self.model, messages=api_messages, tools=None,
|
||||||
is_oauth=getattr(self, '_is_anthropic_oauth', False),
|
is_oauth=getattr(self, '_is_anthropic_oauth', False),
|
||||||
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config)
|
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config,
|
||||||
|
preserve_dots=self._anthropic_preserve_dots())
|
||||||
retry_response = self._anthropic_messages_create(_ant_kw2)
|
retry_response = self._anthropic_messages_create(_ant_kw2)
|
||||||
_retry_msg, _ = _nar2(retry_response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
|
_retry_msg, _ = _nar2(retry_response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
|
||||||
final_response = (_retry_msg.content or "").strip()
|
final_response = (_retry_msg.content or "").strip()
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,12 @@ class TestNormalizeModelName:
|
||||||
assert normalize_model_name("claude-opus-4-6") == "claude-opus-4-6"
|
assert normalize_model_name("claude-opus-4-6") == "claude-opus-4-6"
|
||||||
assert normalize_model_name("claude-opus-4-5-20251101") == "claude-opus-4-5-20251101"
|
assert normalize_model_name("claude-opus-4-5-20251101") == "claude-opus-4-5-20251101"
|
||||||
|
|
||||||
|
def test_preserve_dots_for_alibaba_dashscope(self):
|
||||||
|
"""Alibaba/DashScope use dots in model names (e.g. qwen3.5-plus). Fixes #1739."""
|
||||||
|
assert normalize_model_name("qwen3.5-plus", preserve_dots=True) == "qwen3.5-plus"
|
||||||
|
assert normalize_model_name("anthropic/qwen3.5-plus", preserve_dots=True) == "qwen3.5-plus"
|
||||||
|
assert normalize_model_name("qwen3.5-flash", preserve_dots=True) == "qwen3.5-flash"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tool conversion
|
# Tool conversion
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue