diff --git a/frontend/src/components/pages/models.tsx b/frontend/src/components/pages/models.tsx index 68a846cb..dd645ef4 100644 --- a/frontend/src/components/pages/models.tsx +++ b/frontend/src/components/pages/models.tsx @@ -110,6 +110,26 @@ const LOCAL_PROVIDER_CONFIGS: Record> = { category: ["llm", "embedding"], defaultBaseUrl: "https://api.openai.com/v1" }, + "zai-coding-plan": { + icon: Z.AI, + category: ["llm"], + defaultBaseUrl: "https://api.z.ai/api/coding/paas/v4" + }, + "zhipuai-coding-plan": { + icon: Zhipu, + category: ["llm"], + defaultBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" + }, + "alibaba-coding-plan": { + icon: Alibaba Bailian, + category: ["llm"], + defaultBaseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1" + }, + "alibaba-coding-plan-cn": { + icon: Alibaba Bailian, + category: ["llm"], + defaultBaseUrl: "https://coding.dashscope.aliyuncs.com/v1" + }, azure_openai: { icon: , category: ["llm"] diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 8d9e81ea..11c49755 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1242,6 +1242,18 @@ Build when you need.` openai: { description: "Access GPT-4o, GPT-4 Turbo, and DALL-E 3 models. Industry standard for reasoning and creativity.", }, + "zai-coding-plan": { + description: "GLM Coding Plan endpoints via Z.AI.", + }, + "zhipuai-coding-plan": { + description: "GLM Coding Plan endpoints via Zhipu AI.", + }, + "alibaba-coding-plan": { + description: "Alibaba Bailian Coding Plan endpoint.", + }, + "alibaba-coding-plan-cn": { + description: "Alibaba Bailian Coding Plan endpoint (China).", + }, azure_openai: { description: "Enterprise-grade OpenAI models hosted on Azure.", }, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 31c2390c..7022baa1 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1242,6 +1242,18 @@ Build when you need.` openai: { description: "访问 GPT-4o、GPT-4 Turbo 和 DALL-E 3 模型。推理和创造力的行业标准。", }, + "zai-coding-plan": { + description: "Z.AI GLM Coding Plan 端点。", + }, + "zhipuai-coding-plan": { + description: "智谱 GLM Coding Plan 端点(中国区)。", + }, + "alibaba-coding-plan": { + description: "阿里云百炼 Coding Plan 端点。", + }, + "alibaba-coding-plan-cn": { + description: "阿里云百炼 Coding Plan 端点(中国区)。", + }, azure_openai: { description: "Azure 托管的企业级 OpenAI 模型。", }, diff --git a/src/xagent/core/model/chat/basic/adapter.py b/src/xagent/core/model/chat/basic/adapter.py index 46ffd5cd..8490ef3a 100644 --- a/src/xagent/core/model/chat/basic/adapter.py +++ b/src/xagent/core/model/chat/basic/adapter.py @@ -1,6 +1,10 @@ import os from ....model import ChatModelConfig, ModelConfig +from ....model.providers import ( + default_base_url_for_provider, + provider_compatibility_for_provider, +) from ....retry import create_retry_wrapper from ..error import retry_on from .azure_openai import AzureOpenAILLM @@ -19,11 +23,25 @@ def create_base_llm(model: ModelConfig) -> BaseLLM: if not isinstance(model, ChatModelConfig): raise TypeError(f"Invalid model type: {type(model).__name__}") - if model.model_provider == "openai": + compatibility = provider_compatibility_for_provider(model.model_provider) + + if compatibility == "openai_compatible": llm: BaseLLM = OpenAILLM( model_name=model.model_name, api_key=model.api_key, - base_url=model.base_url, + base_url=model.base_url + or default_base_url_for_provider(model.model_provider), + default_temperature=model.default_temperature, + default_max_tokens=model.default_max_tokens, + timeout=model.timeout, + abilities=model.abilities, + ) + elif compatibility == "claude_compatible": + llm = ClaudeLLM( + model_name=model.model_name, + api_key=model.api_key, + base_url=model.base_url + or default_base_url_for_provider(model.model_provider), default_temperature=model.default_temperature, default_max_tokens=model.default_max_tokens, timeout=model.timeout, @@ -60,16 +78,6 @@ def create_base_llm(model: ModelConfig) -> BaseLLM: timeout=model.timeout, abilities=model.abilities, ) - elif model.model_provider == "claude": - llm = ClaudeLLM( - model_name=model.model_name, - api_key=model.api_key, - base_url=model.base_url, - default_temperature=model.default_temperature, - default_max_tokens=model.default_max_tokens, - timeout=model.timeout, - abilities=model.abilities, - ) elif model.model_provider == "xinference": llm = XinferenceLLM( model_name=model.model_name, diff --git a/src/xagent/core/model/chat/langchain.py b/src/xagent/core/model/chat/langchain.py index ca0b2319..9d8bd3bd 100644 --- a/src/xagent/core/model/chat/langchain.py +++ b/src/xagent/core/model/chat/langchain.py @@ -10,6 +10,10 @@ from langchain_openai import AzureChatOpenAI, ChatOpenAI from ...model import ChatModelConfig, ModelConfig +from ...model.providers import ( + default_base_url_for_provider, + provider_compatibility_for_provider, +) from ...retry import ExponentialBackoff, RetryStrategy, create_retry_wrapper from .error import retry_on @@ -97,14 +101,16 @@ def create_base_chat_model( raise TypeError(f"Unsupported Chat model type: {type(model).__name__}") temp = temperature if temperature is not None else model.default_temperature + compatibility = provider_compatibility_for_provider(model.model_provider) - if model.model_provider == "openai": + if compatibility == "openai_compatible": return ChatOpenAI( model=model.model_name, temperature=temp, max_tokens=model.default_max_tokens, api_key=model.api_key, - base_url=model.base_url, + base_url=model.base_url + or default_base_url_for_provider(model.model_provider), timeout=model.timeout, ) elif model.model_provider == "zhipu": diff --git a/src/xagent/core/model/providers.py b/src/xagent/core/model/providers.py new file mode 100644 index 00000000..2646516c --- /dev/null +++ b/src/xagent/core/model/providers.py @@ -0,0 +1,146 @@ +from typing import Any, Optional + +_PROVIDER_ALIASES: dict[str, str] = { + "zai_coding_plan": "zai-coding-plan", + "zhipuai_coding_plan": "zhipuai-coding-plan", + "alibaba_coding_plan": "alibaba-coding-plan", + "alibaba_coding_plan_cn": "alibaba-coding-plan-cn", +} + +# Provider default base URLs used when callers omit an explicit base URL. +_DEFAULT_BASE_URL_BY_PROVIDER: dict[str, str] = { + "openai": "https://api.openai.com/v1", + "dashscope": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "zhipu": "https://open.bigmodel.cn/api/paas/v4", + # Opencode / models.dev naming + "zai-coding-plan": "https://api.z.ai/api/coding/paas/v4", + "zhipuai-coding-plan": "https://open.bigmodel.cn/api/coding/paas/v4", + # Alibaba Bailian (Model Studio) coding plan + "alibaba-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/v1", + "alibaba-coding-plan-cn": "https://coding.dashscope.aliyuncs.com/v1", +} + +_CURATED_MODELS_BY_PROVIDER: dict[str, tuple[str, ...]] = { + "alibaba-coding-plan": ( + "glm-4.7", + "glm-5", + "qwen3-coder-next", + "qwen3-coder-plus", + "qwen3-max-2026-01-23", + "qwen3.5-plus", + ), + "alibaba-coding-plan-cn": ( + "glm-4.7", + "glm-5", + "qwen3-coder-next", + "qwen3-coder-plus", + "qwen3-max-2026-01-23", + "qwen3.5-plus", + ), +} + +_SUPPORTED_PROVIDER_METADATA: tuple[dict[str, Any], ...] = ( + { + "id": "openai", + "name": "OpenAI", + "description": "OpenAI API compatible models", + "requires_base_url": False, + "compatibility": "openai_compatible", + }, + { + "id": "claude", + "name": "Anthropic Claude", + "description": "Anthropic's Claude models", + "requires_base_url": False, + "compatibility": "claude_compatible", + }, + { + "id": "gemini", + "name": "Google Gemini", + "description": "Google's Gemini models", + "requires_base_url": False, + }, + { + "id": "xinference", + "name": "Xinference", + "description": "Xinference models for local inference", + "requires_base_url": True, + }, + { + "id": "dashscope", + "name": "DashScope", + "description": "Alibaba Cloud's DashScope models", + "requires_base_url": False, + }, + { + "id": "alibaba-coding-plan", + "name": "Alibaba Coding Plan", + "description": "Alibaba Bailian (Model Studio) coding plan", + "requires_base_url": False, + "compatibility": "openai_compatible", + }, + { + "id": "alibaba-coding-plan-cn", + "name": "Alibaba Coding Plan (China)", + "description": "Alibaba Bailian (Model Studio) coding plan (China)", + "requires_base_url": False, + "compatibility": "openai_compatible", + }, + { + "id": "zhipu", + "name": "Zhipu AI", + "description": "Zhipu AI models (GLM series) using zai SDK", + "requires_base_url": False, + }, + { + "id": "zai-coding-plan", + "name": "Z.AI Coding Plan", + "description": "GLM coding plan via Z.AI", + "requires_base_url": False, + "compatibility": "openai_compatible", + }, + { + "id": "zhipuai-coding-plan", + "name": "Zhipu AI Coding Plan", + "description": "GLM coding plan via Zhipu AI", + "requires_base_url": False, + "compatibility": "openai_compatible", + }, +) + + +def _normalize_provider(provider: str) -> str: + return provider.lower().strip() + + +def canonical_provider_name(provider: str) -> str: + normalized = _normalize_provider(provider) + return _PROVIDER_ALIASES.get(normalized, normalized) + + +def default_base_url_for_provider(provider: str) -> Optional[str]: + return _DEFAULT_BASE_URL_BY_PROVIDER.get(canonical_provider_name(provider)) + + +def curated_models_for_provider(provider: str) -> tuple[str, ...]: + return _CURATED_MODELS_BY_PROVIDER.get(canonical_provider_name(provider), ()) + + +def provider_compatibility_for_provider(provider: str) -> Optional[str]: + provider_id = canonical_provider_name(provider) + for provider_info in _SUPPORTED_PROVIDER_METADATA: + if provider_info["id"] == provider_id: + compatibility = provider_info.get("compatibility") + return str(compatibility) if compatibility is not None else None + return None + + +def get_supported_provider_metadata() -> list[dict[str, Any]]: + providers: list[dict[str, Any]] = [] + for provider in _SUPPORTED_PROVIDER_METADATA: + provider_info = dict(provider) + default_base_url = default_base_url_for_provider(provider_info["id"]) + if default_base_url is not None: + provider_info["default_base_url"] = default_base_url + providers.append(provider_info) + return providers diff --git a/src/xagent/web/api/model.py b/src/xagent/web/api/model.py index 055eb831..9b4faf12 100644 --- a/src/xagent/web/api/model.py +++ b/src/xagent/web/api/model.py @@ -13,6 +13,7 @@ ImageModelConfig, ModelConfig, ) +from xagent.core.model.providers import default_base_url_for_provider from xagent.core.utils.security import redact_sensitive_text from ..auth_dependencies import get_current_user @@ -59,12 +60,14 @@ async def create_model( detail="Only administrators can share models with all users", ) + base_url = model.base_url or default_base_url_for_provider(model.model_provider) + if model.category == "llm": config: ModelConfig = ChatModelConfig( id=model.model_id, model_name=model.model_name, model_provider=model.model_provider, - base_url=model.base_url, + base_url=base_url, api_key=model.api_key, default_temperature=model.temperature, timeout=180.0, @@ -76,7 +79,7 @@ async def create_model( id=model.model_id, model_name=model.model_name, model_provider=model.model_provider, - base_url=model.base_url, + base_url=base_url, api_key=model.api_key, timeout=180.0, abilities=model.abilities, @@ -88,7 +91,7 @@ async def create_model( id=model.model_id, model_name=model.model_name, model_provider=model.model_provider, - base_url=model.base_url, + base_url=base_url, api_key=model.api_key, default_temperature=model.temperature, timeout=180.0, diff --git a/src/xagent/web/schemas/model.py b/src/xagent/web/schemas/model.py index 35631ae9..79621be4 100644 --- a/src/xagent/web/schemas/model.py +++ b/src/xagent/web/schemas/model.py @@ -334,6 +334,8 @@ class ProviderInfo(BaseModel): name: str description: str requires_base_url: bool + default_base_url: Optional[str] = None + compatibility: Optional[str] = None class SupportedProvidersResponse(BaseModel): diff --git a/src/xagent/web/services/model_list_service.py b/src/xagent/web/services/model_list_service.py index 3ae9fe4b..a1e92d13 100644 --- a/src/xagent/web/services/model_list_service.py +++ b/src/xagent/web/services/model_list_service.py @@ -3,11 +3,20 @@ import logging from typing import Any, Dict, List, Optional +from ...core.model.providers import ( + curated_models_for_provider, + default_base_url_for_provider, + get_supported_provider_metadata, +) from ...core.utils.security import redact_sensitive_text logger = logging.getLogger(__name__) +def _static_model_list(models: tuple[str, ...], owned_by: str) -> List[Dict[str, Any]]: + return [{"id": model_id, "created": 0, "owned_by": owned_by} for model_id in models] + + async def fetch_openai_models( api_key: str, base_url: Optional[str] = None ) -> List[Dict[str, Any]]: @@ -96,6 +105,28 @@ async def fetch_xinference_models( return await XinferenceLLM.list_available_models(base_url=base_url, api_key=api_key) +async def fetch_alibaba_coding_plan_models( + api_key: str, base_url: Optional[str] = None +) -> List[Dict[str, Any]]: + """Return curated Alibaba Bailian coding plan models.""" + _ = api_key, base_url + return _static_model_list( + curated_models_for_provider("alibaba-coding-plan"), + owned_by="alibaba-coding-plan", + ) + + +async def fetch_alibaba_coding_plan_cn_models( + api_key: str, base_url: Optional[str] = None +) -> List[Dict[str, Any]]: + """Return curated Alibaba Bailian coding plan models (China).""" + _ = api_key, base_url + return _static_model_list( + curated_models_for_provider("alibaba-coding-plan-cn"), + owned_by="alibaba-coding-plan-cn", + ) + + # Provider registry mapping provider names to their fetch functions PROVIDER_FETCHERS: Dict[str, Any] = { "openai": fetch_openai_models, @@ -105,6 +136,10 @@ async def fetch_xinference_models( "gemini": fetch_gemini_models, "google": fetch_gemini_models, "xinference": fetch_xinference_models, + "zai-coding-plan": fetch_openai_models, + "zhipuai-coding-plan": fetch_openai_models, + "alibaba-coding-plan": fetch_alibaba_coding_plan_models, + "alibaba-coding-plan-cn": fetch_alibaba_coding_plan_cn_models, } @@ -123,14 +158,16 @@ async def fetch_models_from_provider( Returns: List of available models """ - fetcher = PROVIDER_FETCHERS.get(provider.lower()) + provider_id = provider.lower() + fetcher = PROVIDER_FETCHERS.get(provider_id) if not fetcher: logger.warning(f"Unknown provider: {provider}") return [] try: - result: List[Dict[str, Any]] = await fetcher(api_key, base_url) + resolved_base_url = base_url or default_base_url_for_provider(provider_id) + result: List[Dict[str, Any]] = await fetcher(api_key, resolved_base_url) return result except Exception as e: logger.error( @@ -147,42 +184,4 @@ def get_supported_providers() -> List[Dict[str, Any]]: Returns: List of provider information """ - return [ - { - "id": "openai", - "name": "OpenAI", - "description": "OpenAI API compatible models", - "requires_base_url": False, - }, - { - "id": "claude", - "name": "Anthropic Claude", - "description": "Anthropic's Claude models", - "requires_base_url": False, - }, - { - "id": "gemini", - "name": "Google Gemini", - "description": "Google's Gemini models", - "requires_base_url": False, - }, - { - "id": "xinference", - "name": "Xinference", - "description": "Xinference models for local inference", - "requires_base_url": True, - }, - { - "id": "zhipu", - "name": "Zhipu AI", - "description": "Zhipu AI models (GLM series) using zai SDK", - "requires_base_url": False, - }, - { - "id": "dashscope", - "name": "DashScope", - "description": "Alibaba Cloud's DashScope models", - "requires_base_url": False, - "default_base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", - }, - ] + return get_supported_provider_metadata()