feat: add Alibaba Coding Plan provider with .env base_url support (#200)

* feat(providers): 新增 Alibaba Cloud (Coding Plan) 内置 provider

对齐 hermes-agent 上游 PR #15045(commit 727d1088),新增
alibaba-coding-plan provider,鉴权使用 ALIBABA_CODING_PLAN_API_KEY
环境变量,base_url 可通过 ALIBABA_CODING_PLAN_BASE_URL 覆盖。

默认 base_url 使用国际版端点 coding-intl.dashscope.aliyuncs.com/v1,
与上游 auth.py:255 保持一致。中国大陆 DashScope 账号
(dashscope.aliyun.com 颁发的 sk-sp-* 密钥)需要通过
ALIBABA_CODING_PLAN_BASE_URL=https://coding.dashscope.aliyuncs.com/v1
(不带 -intl)覆盖,因为 -intl 端点对该类密钥返回 HTTP 401。
该差异在源码注释中已说明。

模型列表覆盖 8 个 Coding Plan 支持的模型:qwen3.5-plus、
qwen3-max-2026-01-23、qwen3-coder-next/plus、glm-5、glm-4.7、
kimi-k2.5、MiniMax-M2.5(基于实测可用列表)。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(providers): Alibaba Coding Plan 添加国内/国际区域切换

在 ProviderFormModal 中针对 alibaba-coding-plan preset 增加一个
"区域"字段,可在国际版(coding-intl)与中国大陆(coding,无 -intl)
两个端点之间切换,切换时自动更新 base_url。

默认选中国际版以对齐上游 hermes-agent 默认值。中国大陆 DashScope
账号(dashscope.aliyun.com 颁发的 sk-sp-* 密钥)只需在表单里点一下
"中国大陆"即可,无需手动改 base_url 或设环境变量。

8 个 locale(zh/en/de/es/fr/ja/ko/pt)都补全了 region/regionIntl/
regionCn 三个 i18n key。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(providers): builtin provider 列表优先读取 base_url env override

之前服务端 getAvailable 在渲染 builtin provider 列表时直接
用 PROVIDER_PRESETS 里的默认 base_url,忽略了用户保存到 .env
的 base_url override。这导致用户在 Alibaba Coding Plan 选了"中国
大陆"保存后,列表里仍然显示国际版 URL。

修复:envMapping.base_url_env 如果存在且 .env 中有值,优先
使用该值;否则 fallback 到 preset 默认。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
ww
2026-04-25 14:00:07 +08:00
committed by GitHub
parent 12ae840234
commit 4bdcaa6258
13 changed files with 92 additions and 2 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch, computed, onMounted } from 'vue'
import { NModal, NForm, NFormItem, NInput, NInputNumber, NButton, NSelect, useMessage } from 'naive-ui'
import { NModal, NForm, NFormItem, NInput, NInputNumber, NButton, NSelect, NRadioGroup, NRadioButton, useMessage } from 'naive-ui'
import { useModelsStore } from '@/stores/hermes/models'
import { useI18n } from 'vue-i18n'
import CodexLoginModal from './CodexLoginModal.vue'
@@ -36,9 +36,16 @@ const modelOptions = ref<Array<{ label: string; value: string }>>([])
const CODEX_KEY = 'openai-codex'
const NOUS_KEY = 'nous'
const ALIBABA_CODING_KEY = 'alibaba-coding-plan'
const ALIBABA_CODING_REGIONS = {
intl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
cn: 'https://coding.dashscope.aliyuncs.com/v1',
} as const
const isCodex = computed(() => selectedPreset.value === CODEX_KEY)
const isNous = computed(() => selectedPreset.value === NOUS_KEY)
const isAlibabaCoding = computed(() => selectedPreset.value === ALIBABA_CODING_KEY)
const alibabaCodingRegion = ref<'intl' | 'cn'>('intl')
const presetOptions = computed(() =>
modelsStore.allProviders.map(g => ({ label: g.label, value: g.provider })),
@@ -55,6 +62,7 @@ function autoGenerateName(url: string): string {
watch(selectedPreset, (val) => {
formData.value.model = ''
alibabaCodingRegion.value = 'intl'
if (val) {
const group = modelsStore.allProviders.find(g => g.provider === val)
if (group) {
@@ -68,6 +76,12 @@ watch(selectedPreset, (val) => {
}
})
watch(alibabaCodingRegion, (region) => {
if (isAlibabaCoding.value) {
formData.value.base_url = ALIBABA_CODING_REGIONS[region]
}
})
watch(() => formData.value.base_url, (url) => {
if (providerType.value === 'custom' && url.trim() && !formData.value.name) {
formData.value.name = autoGenerateName(url.trim())
@@ -236,6 +250,13 @@ function handleClose() {
/>
</NFormItem>
<NFormItem v-if="isAlibabaCoding" :label="t('models.region')">
<NRadioGroup v-model:value="alibabaCodingRegion">
<NRadioButton value="intl">{{ t('models.regionIntl') }}</NRadioButton>
<NRadioButton value="cn">{{ t('models.regionCn') }}</NRadioButton>
</NRadioGroup>
</NFormItem>
<NFormItem v-if="!isCodex && !isNous" :label="t('models.baseUrl')" required>
<NInput
v-model:value="formData.base_url"
+3
View File
@@ -238,6 +238,9 @@ export default {
name: 'Name',
autoGeneratedName: 'Automatisch aus Basis-URL generiert',
baseUrl: 'Basis-URL',
region: 'Region',
regionIntl: 'International',
regionCn: 'Festlandchina',
baseUrlPlaceholder: 'z. B. https://api.example.com/v1',
apiKey: 'API-Schlussel',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -262,6 +262,9 @@ export default {
name: 'Name',
autoGeneratedName: 'Auto-generated from Base URL',
baseUrl: 'Base URL',
region: 'Region',
regionIntl: 'International',
regionCn: 'Mainland China',
baseUrlPlaceholder: 'e.g. https://api.example.com/v1',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -238,6 +238,9 @@ export default {
name: 'Nombre',
autoGeneratedName: 'Generado automaticamente desde la URL base',
baseUrl: 'URL base',
region: 'Región',
regionIntl: 'Internacional',
regionCn: 'China continental',
baseUrlPlaceholder: 'ej. https://api.example.com/v1',
apiKey: 'Clave API',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -238,6 +238,9 @@ export default {
name: 'Nom',
autoGeneratedName: 'Genere automatiquement a partir de l\'URL de base',
baseUrl: 'URL de base',
region: 'Région',
regionIntl: 'International',
regionCn: 'Chine continentale',
baseUrlPlaceholder: 'ex. https://api.example.com/v1',
apiKey: 'Cle API',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -238,6 +238,9 @@ export default {
name: '名前',
autoGeneratedName: 'ベース URL から自動生成',
baseUrl: 'ベース URL',
region: 'リージョン',
regionIntl: 'インターナショナル',
regionCn: '中国本土',
baseUrlPlaceholder: '例: https://api.example.com/v1',
apiKey: 'API キー',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -238,6 +238,9 @@ export default {
name: '이름',
autoGeneratedName: 'Base URL에서 자동 생성',
baseUrl: 'Base URL',
region: '지역',
regionIntl: '국제판',
regionCn: '중국 본토',
baseUrlPlaceholder: '예: https://api.example.com/v1',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -238,6 +238,9 @@ export default {
name: 'Nome',
autoGeneratedName: 'Gerado automaticamente pela URL base',
baseUrl: 'URL base',
region: 'Região',
regionIntl: 'Internacional',
regionCn: 'China Continental',
baseUrlPlaceholder: 'ex. https://api.example.com/v1',
apiKey: 'Chave API',
apiKeyPlaceholder: 'sk-...',
+3
View File
@@ -262,6 +262,9 @@ export default {
name: '名称',
autoGeneratedName: '根据 Base URL 自动生成',
baseUrl: 'Base URL',
region: '区域',
regionIntl: '国际版',
regionCn: '中国大陆',
baseUrlPlaceholder: '例如 https://api.example.com/v1',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-...',
+20
View File
@@ -120,6 +120,26 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
'MiniMax-M2.5',
],
},
{
label: 'Alibaba Cloud (Coding Plan)',
value: 'alibaba-coding-plan',
// NOTE: This is the international (intl) DashScope endpoint, matching upstream
// hermes-agent (auth.py:255). Mainland China DashScope accounts (sk-sp-* keys
// issued by dashscope.aliyun.com) must override via ALIBABA_CODING_PLAN_BASE_URL=
// https://coding.dashscope.aliyuncs.com/v1 (no -intl), since the -intl endpoint
// returns HTTP 401 for those keys.
base_url: 'https://coding-intl.dashscope.aliyuncs.com/v1',
models: [
'qwen3.5-plus',
'qwen3-max-2026-01-23',
'qwen3-coder-next',
'qwen3-coder-plus',
'glm-5',
'glm-4.7',
'kimi-k2.5',
'MiniMax-M2.5',
],
},
{
label: 'Hugging Face',
value: 'huggingface',
@@ -62,7 +62,10 @@ export async function getAvailable(ctx: any) {
if (!envMapping.api_key_env && !isOAuthAuthorized(providerKey)) continue
const preset = PROVIDER_PRESETS.find((p: any) => p.value === providerKey)
const label = preset?.label || providerKey.replace(/^custom:/, '')
const baseUrl = preset?.base_url || ''
let baseUrl = preset?.base_url || ''
if (envMapping.base_url_env && envHasValue(envMapping.base_url_env)) {
baseUrl = envGetValue(envMapping.base_url_env) || baseUrl
}
const catalogModels = PROVIDER_MODEL_CATALOG[providerKey]
if (catalogModels && catalogModels.length > 0) {
const apiKey = envMapping.api_key_env ? envGetValue(envMapping.api_key_env) : ''
@@ -17,6 +17,7 @@ export const PROVIDER_ENV_MAP: Record<string, { api_key_env: string; base_url_en
'minimax-cn': { api_key_env: 'MINIMAX_CN_API_KEY', base_url_env: '' },
deepseek: { api_key_env: 'DEEPSEEK_API_KEY', base_url_env: '' },
alibaba: { api_key_env: 'DASHSCOPE_API_KEY', base_url_env: '' },
'alibaba-coding-plan': { api_key_env: 'ALIBABA_CODING_PLAN_API_KEY', base_url_env: 'ALIBABA_CODING_PLAN_BASE_URL' },
anthropic: { api_key_env: 'ANTHROPIC_API_KEY', base_url_env: '' },
xai: { api_key_env: 'XAI_API_KEY', base_url_env: '' },
xiaomi: { api_key_env: 'XIAOMI_API_KEY', base_url_env: '' },
+21
View File
@@ -128,6 +128,27 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
'MiniMax-M2.5',
],
},
{
label: 'Alibaba Cloud (Coding Plan)',
value: 'alibaba-coding-plan',
builtin: true,
// NOTE: This is the international (intl) DashScope endpoint, matching upstream
// hermes-agent (auth.py:255). Mainland China DashScope accounts (sk-sp-* keys
// issued by dashscope.aliyun.com) must override via ALIBABA_CODING_PLAN_BASE_URL=
// https://coding.dashscope.aliyuncs.com/v1 (no -intl), since the -intl endpoint
// returns HTTP 401 for those keys.
base_url: 'https://coding-intl.dashscope.aliyuncs.com/v1',
models: [
'qwen3.5-plus',
'qwen3-max-2026-01-23',
'qwen3-coder-next',
'qwen3-coder-plus',
'glm-5',
'glm-4.7',
'kimi-k2.5',
'MiniMax-M2.5',
],
},
{
label: 'Hugging Face',
value: 'huggingface',