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:
@@ -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"
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -238,6 +238,9 @@ export default {
|
||||
name: '名前',
|
||||
autoGeneratedName: 'ベース URL から自動生成',
|
||||
baseUrl: 'ベース URL',
|
||||
region: 'リージョン',
|
||||
regionIntl: 'インターナショナル',
|
||||
regionCn: '中国本土',
|
||||
baseUrlPlaceholder: '例: https://api.example.com/v1',
|
||||
apiKey: 'API キー',
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -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-...',
|
||||
|
||||
@@ -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: '' },
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user