feat: Add support for text-to-image APIs. (#13380)

* feat: Add support for text-to-image APIs.

* feat: Add support for text-to-image APIs.
This commit is contained in:
CityFun
2026-07-28 09:42:23 +08:00
committed by GitHub
parent c04848a92a
commit e2754b447d
57 changed files with 1124 additions and 163 deletions
+6
View File
@@ -556,8 +556,10 @@ export namespace AI {
apiType: string;
baseUrl: string;
editableBaseUrl: boolean;
supportsModelDiscovery: boolean;
defaultAuthMode: string;
authModes: string[];
models: ProviderModelInfo[];
}
export interface ProviderInfo {
@@ -579,6 +581,7 @@ export namespace AI {
authMode: string;
models?: AgentAccountModel[];
verifyModel: string;
validateAvailability: boolean;
remark: string;
}
@@ -591,6 +594,7 @@ export namespace AI {
apiType: string;
authMode: string;
verifyModel: string;
validateAvailability: boolean;
remark: string;
syncAgents: boolean;
}
@@ -599,6 +603,8 @@ export namespace AI {
page: number;
pageSize: number;
provider: string;
apiType?: string;
textOnly?: boolean;
name: string;
}
@@ -0,0 +1,43 @@
<template>
<span class="agent-provider-label">
<ProviderLogo :provider="provider" :display-name="name" :size="size" />
<span class="agent-provider-label__name" :title="name">{{ name }}</span>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import ProviderLogo from '@/components/agent-provider-logo/index.vue';
import { getAgentProviderDisplayName } from '@/utils/agent';
const props = withDefaults(
defineProps<{
provider: string;
displayName?: string;
size?: number;
}>(),
{
displayName: '',
size: 16,
},
);
const name = computed(() => getAgentProviderDisplayName(props.provider, props.displayName));
</script>
<style scoped lang="scss">
.agent-provider-label {
display: inline-flex;
align-items: center;
max-width: 100%;
min-width: 0;
gap: 6px;
white-space: nowrap;
}
.agent-provider-label__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -0,0 +1,44 @@
<template>
<el-tag
size="small"
effect="dark"
:type="tagType"
:class="{ 'api-type-tag--responses': apiType === 'openai-responses' }"
>
{{ apiType || '-' }}
</el-tag>
</template>
<script setup lang="ts">
import { computed } from 'vue';
type TagType = 'primary' | 'success' | 'warning' | 'info';
const props = defineProps<{
apiType: string;
}>();
const tagType = computed<TagType>(() => {
if (props.apiType === 'openai-completions') {
return 'primary';
}
if (props.apiType === 'openai-responses') {
return 'info';
}
if (props.apiType === 'anthropic-messages') {
return 'warning';
}
if (props.apiType.endsWith('-images')) {
return 'success';
}
return 'info';
});
</script>
<style scoped lang="scss">
.api-type-tag--responses {
--el-tag-bg-color: #7c3aed;
--el-tag-border-color: #7c3aed;
--el-tag-text-color: #ffffff;
}
</style>
+16 -2
View File
@@ -742,6 +742,7 @@ const message = {
manualModelConfiguration: 'Manual setup',
discoverModelsHelper: 'Get models from the provider /v1/models API and add all of them to the model pool',
discoverModelsFailedFallback: 'Failed to get models. Configure a model manually.',
discoverModelsFailedUseDefaults: 'Failed to get models. Default models are being used.',
accountModelsDuplicate: 'Duplicate models were found in the catalog',
modelPool: 'Model Pool',
fallbackModels: 'Fallback Models',
@@ -894,8 +895,13 @@ const message = {
'Saving the username or password will automatically rebuild the container so the changes take effect.',
channelDeleteConfirm: 'Delete the {0} channel configuration?',
apiTypeBaseURLHelper: 'This type sends requests to {0}. Recommended Base URL: {1}',
customImageURLHelper:
'Enter the complete image generation endpoint for Custom accounts. The path is not appended automatically. Example: {0}',
apiTypeBaseURLMismatch:
'The current Base URL looks like a {0} path, but {1} is selected. Recommended URL: {2}',
validateAvailability: 'Validate account availability',
validateAvailabilityHelper: 'Send a minimal message before saving to validate the API',
validateImageAvailabilityHelper: 'Generates one minimal image before saving and may incur charges',
},
model: {
model: 'Models',
@@ -955,6 +961,7 @@ const message = {
},
aiProxy: {
title: 'AI Gateway',
apiReference: 'API Reference',
notInstalled: 'Not installed',
usage: 'Usage',
accountPool: 'Account Pool',
@@ -1076,13 +1083,20 @@ const message = {
validationModelMapEmpty: 'Model mapping names cannot be empty',
validationModelMapDuplicate: 'Request model {0} is duplicated',
modelGroup: 'Model Group',
modelCapability: 'Model Capability',
textGeneration: 'Text Generation',
imageGeneration: 'Image Generation',
textModels: 'Text Models',
textToImageModels: 'Text-to-Image Models',
unauthorized: 'Unauthorized',
imageCount: 'Image Count',
availableModels: 'Available Models',
modelGroupModels: 'Request Models',
modelGroupModelsPlaceholder: 'Select or enter request model names',
modelGroupModelsHelper:
'Only enter real upstream models; smart routing tries models in order and does not load balance between models.',
'Enter the request model names used by clients to call the AI Gateway; smart routing tries models in order and does not load balance between models.',
modelGroupGuide:
'Model groups only contain real upstream models. auto is a virtual model name used by clients when requesting the AI Gateway.',
'Model groups contain request model names exposed by the AI Gateway. Configure upstream models in the account pool model mapping. auto is a virtual model name and does not belong in a model group.',
modelGroupModelCount: '{0} models',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+17 -2
View File
@@ -742,6 +742,8 @@ const message = {
discoverModelsHelper:
'Obtenga los modelos mediante la API /v1/models del proveedor y añádalos todos al conjunto de modelos',
discoverModelsFailedFallback: 'No se pudieron obtener los modelos. Configure un modelo manualmente.',
discoverModelsFailedUseDefaults:
'No se pudieron obtener los modelos. Se usarán los modelos predeterminados.',
accountModelsDuplicate: 'Existen modelos duplicados en el catálogo',
modelPool: 'Pool de modelos',
fallbackModels: 'Modelos de respaldo',
@@ -899,8 +901,13 @@ const message = {
'Al guardar el usuario o la contraseña, el contenedor se reconstruirá automáticamente para que los cambios surtan efecto.',
channelDeleteConfirm: '¿Eliminar la configuración del canal {0}?',
apiTypeBaseURLHelper: 'Este tipo envía solicitudes a {0}. Base URL recomendada: {1}',
customImageURLHelper:
'Introduce la URL completa del endpoint de generación de imágenes para cuentas Custom. La ruta no se añade automáticamente. Ejemplo: {0}',
apiTypeBaseURLMismatch:
'La Base URL actual parece una ruta {0}, pero está seleccionado {1}. URL recomendada: {2}',
validateAvailability: 'Validar disponibilidad de la cuenta',
validateAvailabilityHelper: 'Envía un mensaje mínimo antes de guardar para validar la API',
validateImageAvailabilityHelper: 'Genera una imagen mínima antes de guardar y puede generar costes',
},
model: {
model: 'Modelo',
@@ -962,6 +969,7 @@ const message = {
},
aiProxy: {
title: 'Gateway de IA',
apiReference: 'Referencia de API',
notInstalled: 'No instalado',
usage: 'Uso',
accountPool: 'Grupo de cuentas',
@@ -1085,13 +1093,20 @@ const message = {
validationModelMapEmpty: 'Los nombres del mapeo de modelos no pueden estar vacíos',
validationModelMapDuplicate: 'El modelo solicitado {0} está duplicado',
modelGroup: 'Grupo de modelos',
modelCapability: 'Capacidad del modelo',
textGeneration: 'Generación de texto',
imageGeneration: 'Generación de imágenes',
textModels: 'Modelos de texto',
textToImageModels: 'Modelos de texto a imagen',
unauthorized: 'Sin autorización',
imageCount: 'Cantidad de imágenes',
availableModels: 'Modelos disponibles',
modelGroupModels: 'Modelos solicitados',
modelGroupModelsPlaceholder: 'Seleccione o ingrese nombres de modelos solicitados',
modelGroupModelsHelper:
'Ingrese solo modelos upstream reales; el enrutamiento inteligente prueba los modelos en orden y no balancea carga entre modelos.',
'Ingrese los nombres de modelo que usan los clientes al llamar al gateway de IA; el enrutamiento inteligente prueba los modelos en orden y no balancea carga entre modelos.',
modelGroupGuide:
'Los grupos de modelos solo contienen modelos upstream reales. auto es un nombre de modelo virtual usado por los clientes al llamar al gateway de IA.',
'Los grupos de modelos contienen los nombres de modelo expuestos por el gateway de IA. Configure los modelos upstream en el mapeo de modelos del pool de cuentas. auto es un nombre de modelo virtual y no debe añadirse al grupo.',
modelGroupModelCount: '{0} modelos',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+18 -2
View File
@@ -730,6 +730,7 @@ const message = {
discoverModelsHelper:
'مدل‌ها را از API /v1/models ارائه‌دهنده دریافت و همه را به مجموعه مدل‌ها اضافه کنید',
discoverModelsFailedFallback: 'دریافت مدل‌ها ناموفق بود. مدل را به‌صورت دستی پیکربندی کنید.',
discoverModelsFailedUseDefaults: 'دریافت مدل‌ها ناموفق بود. از مدل‌های پیش‌فرض استفاده می‌شود.',
accountModelsDuplicate: 'مدل‌های تکراری در کاتالوگ یافت شد',
modelPool: 'مجموعه مدل‌ها',
fallbackModels: 'مدل‌های جایگزین',
@@ -878,6 +879,13 @@ const message = {
'برای شروع کار ورود با QR کلیک کنید. کد QR در لاگ وظیفه ظاهر می‌شود و پس از اسکان موفق، کانتینر به طور خودکار مجدداً راه‌اندازی می‌شود.',
channelAutoRestartHelper: 'ذخیره به طور خودکار کانتینر را مجدداً راه‌اندازی می‌کند تا تغییرات اعمال شوند.',
channelDeleteConfirm: 'پیکربندی کانال {0} حذف شود؟',
apiTypeBaseURLHelper: 'این نوع درخواست‌ها را به {0} ارسال می‌کند. Base URL پیشنهادی: {1}',
apiTypeBaseURLMismatch: 'Base URL فعلی شبیه مسیر {0} است، اما {1} انتخاب شده است. URL پیشنهادی: {2}',
customImageURLHelper:
'برای حساب Custom، URL کامل رابط تولید تصویر را وارد کنید. مسیر به‌صورت خودکار افزوده نمی‌شود. نمونه: {0}',
validateAvailability: 'اعتبارسنجی دسترس‌پذیری حساب',
validateAvailabilityHelper: 'پیش از ذخیره یک پیام حداقلی برای بررسی API ارسال می‌شود',
validateImageAvailabilityHelper: 'پیش از ذخیره یک تصویر حداقلی تولید می‌شود و ممکن است هزینه داشته باشد',
},
model: {
model: 'مدل‌ها',
@@ -937,6 +945,7 @@ const message = {
},
aiProxy: {
title: 'دروازه AI',
apiReference: 'راهنمای API',
notInstalled: 'نصب نشده',
usage: 'استفاده',
accountPool: 'مجموعه حساب‌ها',
@@ -1058,13 +1067,20 @@ const message = {
validationModelMapEmpty: 'نام‌های نگاشت مدل نمی‌توانند خالی باشند',
validationModelMapDuplicate: 'مدل درخواست {0} تکراری است',
modelGroup: 'گروه مدل',
modelCapability: 'قابلیت مدل',
textGeneration: 'تولید متن',
imageGeneration: 'تولید تصویر',
textModels: 'مدل‌های متنی',
textToImageModels: 'مدل‌های متن به تصویر',
unauthorized: 'بدون مجوز',
imageCount: 'تعداد تصاویر',
availableModels: 'مدل‌های موجود',
modelGroupModels: 'مدل‌های درخواست',
modelGroupModelsPlaceholder: 'نام‌های مدل درخواست را انتخاب یا وارد کنید',
modelGroupModelsHelper:
'فقط مدل‌های upstream واقعی را وارد کنید؛ مسیریابی هوشمند مدل‌ها را به‌ترتیب امتحان می‌کند و بین مدل‌ها توازن بار انجام نمی‌دهد.',
'نام مدل‌های درخواستی را که کلاینت برای فراخوانی دروازه AI استفاده می‌کند وارد کنید؛ مسیریابی هوشمند مدل‌ها را به‌ترتیب امتحان می‌کند و بین مدل‌ها توازن بار انجام نمی‌دهد.',
modelGroupGuide:
'گروه‌های مدل فقط شامل مدل‌های upstream واقعی هستند. auto نام مدل مجازی است که کلاینت هنگام درخواست به دروازه AI استفاده می‌کند.',
'گروه‌های مدل شامل نام مدل‌های درخواستی ارائه‌شده توسط دروازه AI هستند. مدل‌های upstream را در نگاشت مدل استخر حساب‌ها تنظیم کنید. auto نام مدل مجازی است و نباید به گروه مدل اضافه شود.',
modelGroupModelCount: '{0} مدل',
contentCompliance: 'انطباق محتوا',
contentAction: 'اقدام',
+16 -2
View File
@@ -736,6 +736,7 @@ const message = {
manualModelConfiguration: '手動設定',
discoverModelsHelper: 'プロバイダーの /v1/models API からモデルを取得しすべてモデルプールに追加します',
discoverModelsFailedFallback: 'モデルを取得できませんでした手動でモデルを設定してください',
discoverModelsFailedUseDefaults: 'モデルを取得できませんでしたデフォルトモデルを使用しています',
accountModelsDuplicate: 'カタログに重複したモデルがあります',
modelPool: 'モデルプール',
fallbackModels: 'フォールバックモデル',
@@ -890,7 +891,12 @@ const message = {
'ユーザー名またはパスワードを保存すると変更を反映するためにコンテナが自動で再作成されます',
channelDeleteConfirm: '{0} チャンネルの設定を削除しますか',
apiTypeBaseURLHelper: 'このタイプは最終的に {0} にリクエストします推奨 Base URL: {1}',
customImageURLHelper:
'Custom アカウントには画像生成 API の完全な URL を入力してくださいパスは自動追加されません: {0}',
apiTypeBaseURLMismatch: '現在の Base URL {0} のパスに見えますが{1} が選択されています推奨 URL: {2}',
validateAvailability: 'アカウントの可用性を検証',
validateAvailabilityHelper: '保存前に最小メッセージを送信して API を検証します',
validateImageAvailabilityHelper: '保存前に最小画像を1枚生成するため料金が発生する場合があります',
},
model: {
model: 'モデル',
@@ -950,6 +956,7 @@ const message = {
},
aiProxy: {
title: 'AI ゲートウェイ',
apiReference: 'API リファレンス',
notInstalled: '未インストール',
usage: '使用量',
accountPool: 'アカウントプール',
@@ -1073,13 +1080,20 @@ const message = {
validationModelMapEmpty: 'モデルマッピングのモデル名は空にできません',
validationModelMapDuplicate: 'リクエストモデル {0} が重複しています',
modelGroup: 'モデルグループ',
modelCapability: 'モデル機能',
textGeneration: 'テキスト生成',
imageGeneration: '画像生成',
textModels: 'テキストモデル',
textToImageModels: 'テキスト画像生成モデル',
unauthorized: '未承認',
imageCount: '画像数',
availableModels: '利用可能なモデル',
modelGroupModels: 'リクエストモデル',
modelGroupModelsPlaceholder: 'リクエストモデル名を選択または入力してください',
modelGroupModelsHelper:
'実際の上流モデルのみ入力してくださいスマートルーティングは順番にモデルを試しモデル間の負荷分散は行いません',
'クライアントが AI ゲートウェイを呼び出す際に使用するリクエストモデル名を入力してくださいスマートルーティングは順番にモデルを試しモデル間の負荷分散は行いません',
modelGroupGuide:
'モデルグループには実際の上流モデルだけを設定しますauto はクライアントが AI ゲートウェイにリクエストする時の仮想モデル名です',
'モデルグループには AI ゲートウェイが公開するリクエストモデルを設定します上流モデルはアカウントプールのモデルマッピングで設定してくださいauto は仮想モデル名のためモデルグループには追加しません',
modelGroupModelCount: '{0} モデル',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+16 -2
View File
@@ -727,6 +727,7 @@ const message = {
manualModelConfiguration: '수동 설정',
discoverModelsHelper: '공급자의 /v1/models API에서 모델을 가져와 모두 모델 풀에 추가합니다.',
discoverModelsFailedFallback: '모델을 가져오지 못했습니다. 모델을 직접 설정하세요.',
discoverModelsFailedUseDefaults: '모델을 가져오지 못했습니다. 기본 모델을 사용합니다.',
accountModelsDuplicate: '카탈로그에 중복된 모델이 있습니다',
modelPool: '모델 ',
fallbackModels: '폴백 모델',
@@ -875,7 +876,12 @@ const message = {
'사용자 이름 또는 비밀번호를 저장하면 변경 사항 적용을 위해 컨테이너가 자동으로 재생성됩니다.',
channelDeleteConfirm: '{0} 채널 구성을 삭제하시겠습니까?',
apiTypeBaseURLHelper: ' 유형은 최종적으로 {0} 요청합니다. 권장 Base URL: {1}',
customImageURLHelper:
'Custom 계정에는 전체 이미지 생성 엔드포인트 URL을 입력하세요. 경로는 자동으로 추가되지 않습니다. : {0}',
apiTypeBaseURLMismatch: '현재 Base URL은 {0} 경로처럼 보이지만 {1} 선택되었습니다. 권장 URL: {2}',
validateAvailability: '계정 가용성 확인',
validateAvailabilityHelper: '저장 전에 최소 메시지를 보내 API를 확인합니다',
validateImageAvailabilityHelper: '저장 전에 최소 이미지 장을 생성하며 비용이 발생할 있습니다',
},
model: {
model: '모델',
@@ -934,6 +940,7 @@ const message = {
},
aiProxy: {
title: 'AI 게이트웨이',
apiReference: 'API 안내',
notInstalled: '설치되지 않음',
usage: '사용량',
accountPool: '계정 ',
@@ -1057,13 +1064,20 @@ const message = {
validationModelMapEmpty: '모델 매핑의 모델 이름은 비워 없습니다',
validationModelMapDuplicate: '요청 모델 {0}() 중복되었습니다',
modelGroup: '모델 그룹',
modelCapability: '모델 기능',
textGeneration: '텍스트 생성',
imageGeneration: '이미지 생성',
textModels: '텍스트 모델',
textToImageModels: '텍스트-이미지 모델',
unauthorized: '권한 없음',
imageCount: '이미지 ',
availableModels: '사용 가능한 모델',
modelGroupModels: '요청 모델',
modelGroupModelsPlaceholder: '요청 모델 이름을 선택하거나 입력하세요',
modelGroupModelsHelper:
'실제 업스트림 모델만 입력하세요. 스마트 라우팅은 순서대로 모델을 우선 선택하며 모델 부하 분산은 하지 않습니다.',
'클라이언트가 AI 게이트웨이를 호출할 사용하는 요청 모델 이름을 입력하세요. 스마트 라우팅은 순서대로 모델을 우선 선택하며 모델 부하 분산은 하지 않습니다.',
modelGroupGuide:
'모델 그룹에는 실제 업스트림 모델만 설정합니다. auto는 클라이언트가 AI 게이트웨이에 요청할 사용하 가상 모델 이름니다.',
'모델 그룹에는 AI 게이트웨이가 노출하는 요청 모델 이름을 설정합니다. 업스트림 모델은 계정 풀의 모델 매핑에서 설정하세요. auto 가상 모델 이름이므로 모델 그룹에 추가하지 않습니다.',
modelGroupModelCount: '{0} 모델',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+16 -2
View File
@@ -736,6 +736,7 @@ const message = {
manualModelConfiguration: 'ຕັ້ງຄ່າດ້ວຍຕົນເອງ',
discoverModelsHelper: 'ດຶງໂມເດວຈາກ API /v1/models ຂອງຜູ້ໃຫ້ບໍລິການ ແລະ ເພີ່ມທັງໝົດເຂົ້າໃນຄັງໂມເດວ',
discoverModelsFailedFallback: 'ດຶງລາຍການໂມເດວບໍ່ສຳເລັດ. ກະລຸນາຕັ້ງຄ່າໂມເດວດ້ວຍຕົນເອງ.',
discoverModelsFailedUseDefaults: 'ດຶງລາຍການໂມເດວບໍ່ສຳເລັດ. ກຳລັງໃຊ້ໂມເດວເລີ່ມຕົ້ນ.',
accountModelsDuplicate: 'ພົບໂມເດວທີ່ຊ້ຳກັນໃນລາຍການ',
modelPool: 'ກຸ່ມໂມເດວ (Model Pool)',
fallbackModels: 'ໂມເດວສຳຮອງ (Fallback)',
@@ -887,7 +888,12 @@ const message = {
channelDeleteConfirm: 'ລຶບການຕັ້ງຄ່າຊ່ອງທາງ {0}?',
customProviderHelper: 'ຜູ້ໃຫ້ບໍລິການແບບກຳນົດເອງຈະບໍ່ກວດສອບວ່າບັນຊີສາມາດໃຊ້ງານໄດ້ຫຼືບໍ່.',
apiTypeBaseURLHelper: 'ປະເພດນີ້ຈະສົ່ງການຮ້ອງຂໍໄປທີ່ {0}. Base URL ທີ່ແນະນຳ: {1}',
customImageURLHelper:
'ສຳລັບບັນຊີ Custom ໃຫ້ປ້ອນ URL ຂອງ endpoint ສ້າງຮູບທີ່ຄົບຖ້ວນ. ລະບົບຈະບໍ່ເພີ່ມ path ອັດຕະໂນມັດ. ຕົວຢ່າງ: {0}',
apiTypeBaseURLMismatch: 'Base URL ປັດຈຸບັນເບິ່ງຄືວ່າເປັນເສັ້ນທາງ {0}, ແຕ່ໄດ້ເລືອກ {1}. URL ທີ່ແນະນຳ: {2}',
validateAvailability: 'ກວດສອບການພ້ອມໃຊ້ງານຂອງບັນຊີ',
validateAvailabilityHelper: 'ສົ່ງຂໍ້ຄວາມຂະໜາດນ້ອຍກ່ອນບັນທຶກເພື່ອກວດສອບ API',
validateImageAvailabilityHelper: 'ສ້າງຮູບຂະໜາດນ້ອຍໜຶ່ງຮູບກ່ອນບັນທຶກ ແລະ ອາດມີຄ່າໃຊ້ຈ່າຍ',
},
model: {
model: 'ໂມເດວ',
@@ -946,6 +952,7 @@ const message = {
},
aiProxy: {
title: 'AI Gateway',
apiReference: 'ຄູ່ມື API',
notInstalled: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງ',
usage: 'ການໃຊ້ງານ',
accountPool: 'ກຸ່ມບັນຊີ',
@@ -1068,13 +1075,20 @@ const message = {
validateAvailability: 'ກວດສອບການພ້ອມໃຊ້ງານຂອງບັນຊີ',
validateAvailabilityHelper: 'ສົ່ງຂໍ້ຄວາມຂະໜາດນ້ອຍກ່ອນບັນທຶກເພື່ອຢືນຢັນການໃຊ້ງານ API',
modelGroup: 'ກຸ່ມໂມເດວ',
modelCapability: 'ຄວາມສາມາດຂອງໂມເດວ',
textGeneration: 'ການສ້າງຂໍ້ຄວາມ',
imageGeneration: 'ການສ້າງຮູບພາບ',
textModels: 'ໂມເດວຂໍ້ຄວາມ',
textToImageModels: 'ໂມເດວສ້າງຮູບຈາກຂໍ້ຄວາມ',
unauthorized: 'ບໍ່ມີສິດ',
imageCount: 'ຈຳນວນຮູບ',
availableModels: 'ໂມເດວທີ່ມີໃຫ້ໃຊ້',
modelGroupModels: 'ໂມເດວທີ່ຮ້ອງຂໍ',
modelGroupModelsPlaceholder: 'ເລືອກ ຫຼື ປ້ອນຊື່ໂມເດວທີ່ຮ້ອງຂໍ',
modelGroupModelsHelper:
'ປ້ອນສະເພາະໂມເດວຕົ້ນທາງທີ່ມີຢູ່ຈິງ; Smart Route ຈະລອງໂມເດວຕາມລຳດັບ ແລະ ບໍ່ເຮັດ Load Balance ລະຫວ່າງໂມເດວ.',
'ປ້ອນຊື່ໂມເດວຄຳຂໍທີ່ Client ໃຊ້ເອີ້ນ AI Gateway; Smart Route ຈະລອງໂມເດວຕາມລຳດັບ ແລະ ບໍ່ເຮັດ Load Balance ລະຫວ່າງໂມເດວ.',
modelGroupGuide:
'ກຸ່ມໂມເດວຈະມີສະເພາະໂມເດວຕົ້ນທາງທີ່ມີຢູ່ຈິງ. auto ແມ່ນຊື່ໂມເດວສະເໝືອນທີ່ Client ໃຊ້ເມື່ອຮ້ອງຂໍ AI Gateway.',
'ກຸ່ມໂມເດວຈະມີຊື່ໂມເດວຄຳຂໍທີ່ AI Gateway ເປີດໃຫ້ໃຊ້. ຕັ້ງຄ່າໂມເດວຕົ້ນທາງໃນການຈັບຄູ່ໂມເດວຂອງກຸ່ມບັນຊີ. auto ແມ່ນຊື່ໂມເດວສະເໝືອນ ແລະ ບໍ່ຕ້ອງເພີ່ມໃນກຸ່ມໂມເດວ.',
modelGroupModelCount: '{0} ໂມເດວ',
contentCompliance: 'ການປະຕິບັດຕາມເນື້ອຫາ',
contentAction: 'ການດຳເນີນການ',
+16 -2
View File
@@ -744,6 +744,7 @@ const message = {
discoverModelsHelper:
'Dapatkan model daripada API /v1/models penyedia dan tambahkan semuanya ke katalog model',
discoverModelsFailedFallback: 'Gagal mendapatkan model. Konfigurasikan model secara manual.',
discoverModelsFailedUseDefaults: 'Gagal mendapatkan model. Model lalai sedang digunakan.',
accountModelsDuplicate: 'Terdapat model pendua dalam katalog',
modelPool: 'Katalog model',
fallbackModels: 'Model fallback',
@@ -898,8 +899,13 @@ const message = {
'Menyimpan nama pengguna atau kata laluan akan membina semula bekas secara automatik supaya perubahan berkuat kuasa.',
channelDeleteConfirm: 'Padam konfigurasi saluran {0}?',
apiTypeBaseURLHelper: 'Jenis ini menghantar permintaan ke {0}. Base URL yang disyorkan: {1}',
customImageURLHelper:
'Masukkan URL titik akhir penjanaan imej yang lengkap untuk akaun Custom. Laluan tidak ditambah secara automatik. Contoh: {0}',
apiTypeBaseURLMismatch:
'Base URL semasa kelihatan seperti laluan {0}, tetapi {1} dipilih. URL yang disyorkan: {2}',
validateAvailability: 'Sahkan ketersediaan akaun',
validateAvailabilityHelper: 'Hantar mesej minimum sebelum menyimpan untuk mengesahkan API',
validateImageAvailabilityHelper: 'Menjana satu imej minimum sebelum menyimpan dan mungkin dikenakan caj',
},
model: {
model: 'Model',
@@ -960,6 +966,7 @@ const message = {
},
aiProxy: {
title: 'Gateway AI',
apiReference: 'Rujukan API',
notInstalled: 'Belum dipasang',
usage: 'Penggunaan',
accountPool: 'Kolam Akaun',
@@ -1083,13 +1090,20 @@ const message = {
validationModelMapEmpty: 'Nama model dalam pemetaan model tidak boleh kosong',
validationModelMapDuplicate: 'Model permintaan {0} berulang',
modelGroup: 'Kumpulan Model',
modelCapability: 'Keupayaan Model',
textGeneration: 'Penjanaan Teks',
imageGeneration: 'Penjanaan Imej',
textModels: 'Model Teks',
textToImageModels: 'Model Teks-ke-Imej',
unauthorized: 'Tidak dibenarkan',
imageCount: 'Bilangan Imej',
availableModels: 'Model Tersedia',
modelGroupModels: 'Model Permintaan',
modelGroupModelsPlaceholder: 'Pilih atau masukkan nama model permintaan',
modelGroupModelsHelper:
'Masukkan model upstream sebenar sahaja; penghalaan pintar mencuba model mengikut susunan dan tidak mengimbang beban antara model.',
'Masukkan nama model permintaan yang digunakan klien untuk memanggil AI Gateway; penghalaan pintar mencuba model mengikut susunan dan tidak mengimbang beban antara model.',
modelGroupGuide:
'Kumpulan model hanya mengandungi model upstream sebenar. auto ialah nama model maya yang digunakan klien semasa meminta AI Gateway.',
'Kumpulan model mengandungi nama model permintaan yang didedahkan oleh AI Gateway. Konfigurasikan model upstream dalam pemetaan model kumpulan akaun. auto ialah nama model maya dan tidak perlu ditambah pada kumpulan model.',
modelGroupModelCount: '{0} model',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+16 -2
View File
@@ -738,6 +738,7 @@ const message = {
discoverModelsHelper:
'Obtenha os modelos pela API /v1/models do provedor e adicione todos ao catálogo de modelos',
discoverModelsFailedFallback: 'Não foi possível obter os modelos. Configure um modelo manualmente.',
discoverModelsFailedUseDefaults: 'Não foi possível obter os modelos. Os modelos padrão estão em uso.',
accountModelsDuplicate: ' modelos duplicados no catálogo',
modelPool: 'Catálogo de modelos',
fallbackModels: 'Modelos de fallback',
@@ -894,8 +895,13 @@ const message = {
'Ao salvar o usuário ou a senha, o contêiner será reconstruído automaticamente para que as alterações entrem em vigor.',
channelDeleteConfirm: 'Excluir a configuração do canal {0}?',
apiTypeBaseURLHelper: 'Este tipo envia requisições para {0}. Base URL recomendada: {1}',
customImageURLHelper:
'Informe a URL completa do endpoint de geração de imagens para contas Custom. O caminho não é adicionado automaticamente. Exemplo: {0}',
apiTypeBaseURLMismatch:
'A Base URL atual parece um caminho {0}, mas {1} está selecionado. URL recomendada: {2}',
validateAvailability: 'Validar disponibilidade da conta',
validateAvailabilityHelper: 'Envia uma mensagem mínima antes de salvar para validar a API',
validateImageAvailabilityHelper: 'Gera uma imagem mínima antes de salvar e pode gerar custos',
},
model: {
model: 'Modelo',
@@ -956,6 +962,7 @@ const message = {
},
aiProxy: {
title: 'Gateway de IA',
apiReference: 'Referência da API',
notInstalled: 'Não instalado',
usage: 'Uso',
accountPool: 'Pool de contas',
@@ -1079,13 +1086,20 @@ const message = {
validationModelMapEmpty: 'Os nomes no mapeamento de modelos não podem ficar vazios',
validationModelMapDuplicate: 'O modelo solicitado {0} está duplicado',
modelGroup: 'Grupo de modelos',
modelCapability: 'Capacidade do modelo',
textGeneration: 'Geração de texto',
imageGeneration: 'Geração de imagens',
textModels: 'Modelos de texto',
textToImageModels: 'Modelos de texto para imagem',
unauthorized: 'Não autorizado',
imageCount: 'Quantidade de imagens',
availableModels: 'Modelos disponíveis',
modelGroupModels: 'Modelos solicitados',
modelGroupModelsPlaceholder: 'Selecione ou informe nomes de modelos solicitados',
modelGroupModelsHelper:
'Informe apenas modelos upstream reais; o roteamento inteligente tenta os modelos em ordem e não faz balanceamento entre modelos.',
'Informe os nomes de modelo usados pelos clientes ao chamar o gateway de IA; o roteamento inteligente tenta os modelos em ordem e não faz balanceamento entre modelos.',
modelGroupGuide:
'Grupos de modelos contêm apenas modelos upstream reais. auto é um nome de modelo virtual usado pelos clientes ao chamar o gateway de IA.',
'Grupos de modelos contêm os nomes de modelo expostos pelo gateway de IA. Configure os modelos upstream no mapeamento de modelos do pool de contas. auto é um nome de modelo virtual e não deve ser adicionado ao grupo.',
modelGroupModelCount: '{0} modelos',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+17 -2
View File
@@ -734,6 +734,7 @@ const message = {
manualModelConfiguration: 'Настроить вручную',
discoverModelsHelper: 'Получить модели через API /v1/models поставщика и добавить их все в каталог моделей',
discoverModelsFailedFallback: 'Не удалось получить модели. Настройте модель вручную.',
discoverModelsFailedUseDefaults: 'Не удалось получить модели. Используются модели по умолчанию.',
accountModelsDuplicate: 'В каталоге обнаружены повторяющиеся модели',
modelPool: 'Каталог моделей',
fallbackModels: 'Резервные модели',
@@ -888,7 +889,13 @@ const message = {
'После сохранения имени пользователя или пароля контейнер будет автоматически пересобран, чтобы изменения вступили в силу.',
channelDeleteConfirm: 'Удалить конфигурацию канала {0}?',
apiTypeBaseURLHelper: 'Этот тип отправляет запросы в {0}. Рекомендуемый Base URL: {1}',
customImageURLHelper:
'Для аккаунта Custom укажите полный URL интерфейса генерации изображений. Путь не добавляется автоматически. Пример: {0}',
apiTypeBaseURLMismatch: 'Текущий Base URL похож на путь {0}, но выбран {1}. Рекомендуемый URL: {2}',
validateAvailability: 'Проверять доступность аккаунта',
validateAvailabilityHelper: 'Перед сохранением отправляется минимальное сообщение для проверки API',
validateImageAvailabilityHelper:
'Перед сохранением создаётся одно минимальное изображение; возможна оплата',
},
model: {
model: 'Модель',
@@ -948,6 +955,7 @@ const message = {
},
aiProxy: {
title: 'AI-шлюз',
apiReference: 'Справочник API',
notInstalled: 'Не установлен',
usage: 'Использование',
accountPool: 'Пул аккаунтов',
@@ -1071,13 +1079,20 @@ const message = {
validationModelMapEmpty: 'Имена моделей в сопоставлении не могут быть пустыми',
validationModelMapDuplicate: 'Запрошенная модель {0} дублируется',
modelGroup: 'Группа моделей',
modelCapability: 'Возможность модели',
textGeneration: 'Генерация текста',
imageGeneration: 'Генерация изображений',
textModels: 'Текстовые модели',
textToImageModels: 'Модели генерации изображений по тексту',
unauthorized: 'Нет доступа',
imageCount: 'Количество изображений',
availableModels: 'Доступные модели',
modelGroupModels: 'Запрошенные модели',
modelGroupModelsPlaceholder: 'Выберите или введите имена запрошенных моделей',
modelGroupModelsHelper:
'Указывайте только реальные upstream-модели; умная маршрутизация пробует модели по порядку и не балансирует нагрузку между моделями.',
'Указывайте имена моделей, которые клиенты используют при обращении к AI-шлюзу; умная маршрутизация пробует модели по порядку и не балансирует нагрузку между моделями.',
modelGroupGuide:
'Группы моделей содержат только реальные upstream-модели. auto виртуальное имя модели, которое клиент использует при запросе к AI-шлюзу.',
'Группы моделей содержат имена моделей, доступные клиентам AI-шлюза. Upstream-модели настраиваются в сопоставлении моделей пула аккаунтов. auto виртуальное имя и не добавляется в группу моделей.',
modelGroupModelCount: '{0} моделей',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+16 -2
View File
@@ -740,6 +740,7 @@ const message = {
discoverModelsHelper:
'Sağlayıcının /v1/models APIsinden modelleri alın ve tümünü model kataloğuna ekleyin',
discoverModelsFailedFallback: 'Modeller alınamadı. Bir modeli elle yapılandırın.',
discoverModelsFailedUseDefaults: 'Modeller alınamadı. Varsayılan modeller kullanılıyor.',
accountModelsDuplicate: 'Katalogda yinelenen modeller bulundu',
modelPool: 'Model kataloğu',
fallbackModels: 'Yedek modeller',
@@ -897,8 +898,13 @@ const message = {
'Kullanıcı adı veya parola kaydedildiğinde değişikliklerin etkili olması için konteyner otomatik olarak yeniden oluşturulur.',
channelDeleteConfirm: '{0} kanal yapılandırması silinsin mi?',
apiTypeBaseURLHelper: 'Bu tür istekleri {0} yoluna gönderir. Önerilen Base URL: {1}',
customImageURLHelper:
'Custom hesaplar için tam görsel oluşturma noktası URLsini girin. Yol otomatik eklenmez. Örnek: {0}',
apiTypeBaseURLMismatch:
'Geçerli Base URL bir {0} yolu gibi görünüyor, ancak {1} seçildi. Önerilen URL: {2}',
validateAvailability: 'Hesap kullanılabilirliğini doğrula',
validateAvailabilityHelper: 'Kaydetmeden önce APIyi doğrulamak için en küçük iletiyi gönderir',
validateImageAvailabilityHelper: 'Kaydetmeden önce bir küçük görsel üretir ve ücret doğurabilir',
},
model: {
model: 'Model',
@@ -959,6 +965,7 @@ const message = {
},
aiProxy: {
title: 'AI Geçidi',
apiReference: 'API Referansı',
notInstalled: 'Kurulu değil',
usage: 'Kullanım',
accountPool: 'Hesap Havuzu',
@@ -1082,13 +1089,20 @@ const message = {
validationModelMapEmpty: 'Model eşlemesindeki model adları boş olamaz',
validationModelMapDuplicate: 'İstek modeli {0} yineleniyor',
modelGroup: 'Model Grubu',
modelCapability: 'Model Yeteneği',
textGeneration: 'Metin Üretimi',
imageGeneration: 'Görsel Üretimi',
textModels: 'Metin Modelleri',
textToImageModels: 'Metinden Görsele Modeller',
unauthorized: 'Yetkisiz',
imageCount: 'Görsel Sayısı',
availableModels: 'Kullanılabilir Modeller',
modelGroupModels: 'İstek Modelleri',
modelGroupModelsPlaceholder: 'İstek modeli adlarını seçin veya girin',
modelGroupModelsHelper:
'Yalnızca gerçek üst modelleri girin; akıllı yönlendirme modelleri sırayla dener ve modeller arasında yük dengelemez.',
'İstemcilerin AI Geçidini çağırırken kullandığı istek modeli adlarını girin; akıllı yönlendirme modelleri sırayla dener ve modeller arasında yük dengelemez.',
modelGroupGuide:
'Model grupları yalnızca gerçek üst modelleri içerir. auto, istemcilerin AI Geçidi isteğinde kullandığı sanal model adıdır.',
'Model grupları AI Geçidinin sunduğu istek modeli adlarını içerir. Üst modelleri hesap havuzundaki model eşlemesinde yapılandırın. auto sanal bir model adıdır ve model grubuna eklenmez.',
modelGroupModelCount: '{0} model',
contentCompliance: 'Content Compliance',
contentAction: 'Action',
+16 -2
View File
@@ -703,6 +703,7 @@ const message = {
manualModelConfiguration: '手動配置',
discoverModelsHelper: '從供應商的 /v1/models 介面取得模型取得後將全部加入模型池',
discoverModelsFailedFallback: '取得模型失敗請手動配置模型',
discoverModelsFailedUseDefaults: '取得模型失敗已使用預設模型',
accountModelsDuplicate: '模型池中存在重複模型請檢查後重試',
modelPool: '模型池',
fallbackModels: '備用模型',
@@ -839,7 +840,11 @@ const message = {
dashboardAuthAutoRebuildHelper: '保存使用者名稱或密碼後將自動重建容器以使設定生效',
channelDeleteConfirm: '確認刪除 {0} 頻道設定',
apiTypeBaseURLHelper: '目前類型最終請求 {0}Base URL 建議填寫 {1}',
customImageURLHelper: 'Custom 圖片帳號請填寫完整的圖片產生介面 URL系統不會自動補全路徑例如{0}',
apiTypeBaseURLMismatch: '目前 Base URL 看起來是 {0} 路徑和已選擇的 {1} 不一致建議改為 {2}',
validateAvailability: '驗證帳號可用性',
validateAvailabilityHelper: '儲存前傳送最小訊息驗證介面可用性',
validateImageAvailabilityHelper: '儲存前會實際產生一張最小圖片可能產生費用',
},
model: {
model: '模型',
@@ -896,6 +901,7 @@ const message = {
},
aiProxy: {
title: 'AI 閘道',
apiReference: '介面說明',
notInstalled: '未安裝',
usage: '用量統計',
accountPool: '帳號池',
@@ -1015,12 +1021,20 @@ const message = {
validationModelMapEmpty: '模型映射的模型名稱不能為空',
validationModelMapDuplicate: '請求模型 {0} 重複',
modelGroup: '模型組',
modelCapability: '模型能力',
textGeneration: '文字生成',
imageGeneration: '圖片生成',
textModels: '文字模型',
textToImageModels: '文生圖模型',
unauthorized: '未授權',
imageCount: '圖片數量',
availableModels: '可用模型',
modelGroupModels: '請求模型',
modelGroupModelsPlaceholder: '請選擇或輸入請求模型名稱',
modelGroupModelsHelper: '只填寫真實上游模型智慧路由會按順序優先選擇靠前模型不做模型間負載均衡',
modelGroupModelsHelper:
'填寫用戶端請求 AI 網關時使用的請求模型名稱智慧路由會按順序優先選擇靠前模型不做模型間負載均衡',
modelGroupGuide:
'模型組配置真實上游模型auto 用戶端請求 AI 網關時使用的虛擬模型名稱不需要放進模型組',
'模型組配置用戶端請求 AI 網關時使用的請求模型名稱上游模型在帳號池的模型映射中配置auto 虛擬模型名稱不需要放進模型組',
modelGroupModelCount: '{0} 個模型',
contentCompliance: '內容合規',
contentAction: '處理動作',
+16 -2
View File
@@ -706,6 +706,7 @@ const message = {
manualModelConfiguration: '手动配置',
discoverModelsHelper: '从供应商的 /v1/models 接口获取模型获取后将全部加入模型池',
discoverModelsFailedFallback: '获取模型失败请手动配置模型',
discoverModelsFailedUseDefaults: '获取模型失败已使用默认模型',
accountModelsDuplicate: '模型池中存在重复模型请检查后重试',
modelPool: '模型池',
fallbackModels: '备用模型',
@@ -838,7 +839,11 @@ const message = {
dashboardAuthAutoRebuildHelper: '保存用户名密码后将自动重建容器以使配置生效',
channelDeleteConfirm: '确认删除 {0} 频道配置',
apiTypeBaseURLHelper: '当前类型最终请求 {0}Base URL 建议填写 {1}',
customImageURLHelper: 'Custom 图片账号请填写完整的图片生成接口 URL系统不会自动补全路径例如{0}',
apiTypeBaseURLMismatch: '当前 Base URL 看起来是 {0} 路径和已选择的 {1} 不一致建议改为 {2}',
validateAvailability: '验证账号可用性',
validateAvailabilityHelper: '保存前发送最小消息验证接口可用性',
validateImageAvailabilityHelper: '保存前会真实生成一张最小图片可能产生费用',
},
model: {
model: '模型',
@@ -895,6 +900,7 @@ const message = {
},
aiProxy: {
title: 'AI 网关',
apiReference: '接口说明',
notInstalled: '未安装',
usage: '用量统计',
accountPool: '账号池',
@@ -1012,12 +1018,20 @@ const message = {
validationModelMapEmpty: '模型映射的模型名不能为空',
validationModelMapDuplicate: '请求模型 {0} 重复',
modelGroup: '模型组',
modelCapability: '模型能力',
textGeneration: '文本生成',
imageGeneration: '图片生成',
textModels: '文本模型',
textToImageModels: '文生图模型',
unauthorized: '未授权',
imageCount: '图片数量',
availableModels: '可用模型',
modelGroupModels: '请求模型',
modelGroupModelsPlaceholder: '请选择或输入请求模型名',
modelGroupModelsHelper: '只填写真实上游模型智能路由会按顺序优先选择靠前模型不做模型间负载均衡',
modelGroupModelsHelper:
'填写客户端请求 AI 网关时使用的请求模型名智能路由会按顺序优先选择靠前模型不做模型间负载均衡',
modelGroupGuide:
'模型组配置真实上游模型auto 客户端请求 AI 网关时使用的虚拟模型名不需要放进模型组',
'模型组配置客户端请求 AI 网关时使用的请求模型名上游模型在账号池的模型映射中配置auto 虚拟模型名不需要放进模型组',
modelGroupModelCount: '{0} 个模型',
contentCompliance: '内容合规',
contentAction: '处理动作',
+22 -16
View File
@@ -9,6 +9,24 @@ export interface AgentProviderLogo {
const asset = (name: string) => new URL(`../assets/images/ai-providers/${name}`, import.meta.url).href;
const aliyunLogo: AgentProviderLogo = {
src: asset('aliyun.webp'),
mark: 'QW',
background: '#ffffff',
color: '#ff6a00',
borderColor: '#dcdfe6',
source: 'https://www.alibabacloud.com/',
};
const volcengineLogo: AgentProviderLogo = {
src: asset('volcengine.png'),
mark: 'AR',
background: '#ffffff',
color: '#1664ff',
borderColor: '#dcdfe6',
source: 'https://www.volcengine.com/',
};
const providerLogos: Record<string, AgentProviderLogo> = {
ollama: {
src: asset('ollama.webp'),
@@ -34,22 +52,10 @@ const providerLogos: Record<string, AgentProviderLogo> = {
borderColor: '#dcdfe6',
source: 'https://cdn.deepseek.com/logo.png',
},
'bailian-coding-plan': {
src: asset('aliyun.webp'),
mark: 'QW',
background: '#ffffff',
color: '#ff6a00',
borderColor: '#dcdfe6',
source: 'https://www.alibabacloud.com/',
},
'ark-coding-plan': {
src: asset('volcengine.png'),
mark: 'AR',
background: '#ffffff',
color: '#1664ff',
borderColor: '#dcdfe6',
source: 'https://www.volcengine.com/',
},
bailian: aliyunLogo,
'bailian-coding-plan': aliyunLogo,
ark: volcengineLogo,
'ark-coding-plan': volcengineLogo,
zai: {
src: asset('zai.webp'),
mark: 'Z',
@@ -370,6 +370,7 @@ const loadAccounts = async () => {
page: 1,
pageSize: 200,
provider: form.provider,
textOnly: true,
name: '',
};
const res = await pageAgentAccounts(req);
@@ -168,6 +168,7 @@ const loadAccounts = async () => {
page: 1,
pageSize: 200,
provider: '',
textOnly: true,
name: '',
});
const items = res.data.items || [];
@@ -187,6 +187,7 @@ const load = async (params: AgentModelLoadParams) => {
page: 1,
pageSize: 200,
provider: '',
textOnly: true,
name: '',
}),
getAgentModelConfig({ agentId: params.agentId }),
+235 -36
View File
@@ -20,7 +20,11 @@
</el-select>
</el-form-item>
<el-form-item :label="'API ' + $t('commons.table.type')" prop="apiType">
<el-select v-model="form.apiType" :disabled="apiTypeOptions.length === 1" @change="handleAPITypeChange">
<el-select
v-model="form.apiType"
:disabled="form.id > 0 || apiTypeOptions.length === 1"
@change="handleAPITypeChange"
>
<el-option v-for="item in apiTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
@@ -66,20 +70,37 @@
<el-form-item>
<el-checkbox v-model="form.rememberApiKey">{{ $t('terminal.rememberPassword') }}</el-checkbox>
</el-form-item>
<el-form-item>
<el-switch v-model="form.validateAvailability" />
<span class="ml-2">{{ $t('aiTools.agents.validateAvailability') }}</span>
<span class="input-help">
{{
isImageAPIType
? $t('aiTools.agents.validateImageAvailabilityHelper')
: $t('aiTools.agents.validateAvailabilityHelper')
}}
</span>
</el-form-item>
<template v-if="showModelDiscovery">
<el-divider content-position="left">{{ $t('aiTools.agents.modelPool') }}</el-divider>
<div class="model-discovery">
<div class="model-discovery__header">
<el-radio-group v-model="modelConfigMode" size="small" @change="handleModelConfigModeChange">
<el-radio-button value="discover">
{{ $t('aiTools.agents.automaticModelDiscovery') }}
{{
$t(
supportsModelDiscovery
? 'aiTools.agents.automaticModelDiscovery'
: 'aiTools.agents.defaultModel',
)
}}
</el-radio-button>
<el-radio-button value="manual">
{{ $t('aiTools.agents.manualModelConfiguration') }}
</el-radio-button>
</el-radio-group>
<el-button
v-if="modelConfigMode === 'discover'"
v-if="supportsModelDiscovery && modelConfigMode === 'discover'"
type="primary"
plain
:loading="discovering"
@@ -88,10 +109,16 @@
{{ $t('aiTools.agents.discoverModels') }}
</el-button>
</div>
<div v-if="modelConfigMode === 'discover'" class="input-help model-discovery__help">
<div
v-if="supportsModelDiscovery && modelConfigMode === 'discover'"
class="input-help model-discovery__help"
>
{{ $t('aiTools.agents.discoverModelsHelper') }}
</div>
<div v-if="discoveredModels.length" class="input-help model-discovery__help">
<div
v-if="requiresVerifyModel && modelConfigMode === 'discover' && discoveredModels.length"
class="input-help model-discovery__help"
>
{{ $t('aiTools.agents.verifyModelHelper') }}
</div>
<el-alert
@@ -99,7 +126,13 @@
type="warning"
:closable="false"
show-icon
:title="$t('aiTools.agents.discoverModelsFailedFallback')"
:title="
$t(
defaultModels.length
? 'aiTools.agents.discoverModelsFailedUseDefaults'
: 'aiTools.agents.discoverModelsFailedFallback',
)
"
/>
<el-table
v-if="modelConfigMode === 'discover' && discoveredModels.length"
@@ -107,7 +140,12 @@
border
max-height="320"
>
<el-table-column :label="$t('aiTools.agents.verifyModel')" width="110" align="center">
<el-table-column
v-if="requiresVerifyModel"
:label="$t('aiTools.agents.verifyModel')"
width="110"
align="center"
>
<template #default="{ row }">
<el-radio v-model="form.verifyModel" :value="row.id" />
</template>
@@ -115,6 +153,45 @@
<el-table-column :label="$t('aiTools.model.model')" prop="id" min-width="220" />
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="180" />
</el-table>
<template v-if="modelConfigMode === 'manual'">
<el-table :data="manualModels" border max-height="320">
<el-table-column
v-if="requiresVerifyModel"
:label="$t('aiTools.agents.verifyModel')"
width="110"
align="center"
>
<template #default="{ row, $index }">
<el-radio v-model="manualVerifyIndex" :value="$index" :disabled="!row.id.trim()" />
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.model.model')" min-width="220">
<template #default="{ row }">
<el-input v-model="row.id" />
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.name')" min-width="180">
<template #default="{ row }">
<el-input v-model="row.name" />
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.operate')" width="90" align="center">
<template #default="{ $index }">
<el-button
type="primary"
link
:disabled="manualModels.length === 1"
@click="removeManualModel($index)"
>
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-button class="model-discovery__add" type="primary" plain @click="addManualModel">
{{ $t('commons.button.add') }}
</el-button>
</template>
</div>
</template>
<template v-if="showInitialModel">
@@ -189,6 +266,8 @@ const providers = ref<Record<string, AI.ProviderInfo>>({});
const loading = ref(false);
const discovering = ref(false);
const discoveredModels = ref<AI.AgentAccountModel[]>([]);
const manualModels = ref<AI.AgentAccountModel[]>([]);
const manualVerifyIndex = ref(-1);
const editModels = ref<AI.AgentAccountModel[]>([]);
const modelConfigMode = ref<'discover' | 'manual'>('discover');
const modelDiscoveryFailed = ref(false);
@@ -205,6 +284,7 @@ const form = reactive({
apiKey: '',
rememberApiKey: false,
verifyModel: '',
validateAvailability: true,
initialModel: {} as AI.AgentAccountModel,
remark: '',
syncAgents: false,
@@ -218,22 +298,28 @@ const selectedProvider = computed(() => providers.value[form.provider]);
const selectedAPIConfig = computed(() =>
selectedProvider.value?.apiTypes.find((item) => item.apiType === form.apiType),
);
const showModelDiscovery = computed(
() =>
!form.id &&
form.provider === 'custom' &&
(form.apiType === 'openai-completions' || form.apiType === 'openai-responses'),
);
const isImageAPIType = computed(() => form.apiType.endsWith('-images'));
const supportsModelDiscovery = computed(() => !form.id && Boolean(selectedAPIConfig.value?.supportsModelDiscovery));
const defaultModels = computed(() => {
if (selectedAPIConfig.value?.models.length) {
return selectedAPIConfig.value.models;
}
return isImageAPIType.value ? [] : selectedProvider.value?.models || [];
});
const showModelDiscovery = computed(() => !form.id && (supportsModelDiscovery.value || defaultModels.value.length > 0));
const showInitialModel = computed(
() =>
!form.id &&
initialModelProviders.includes(form.provider) &&
(!showModelDiscovery.value || modelConfigMode.value === 'manual'),
(initialModelProviders.includes(form.provider) || (isImageAPIType.value && defaultModels.value.length === 0)) &&
!showModelDiscovery.value,
);
const verifyModelOptions = computed(() => (form.id ? editModels.value : selectedProvider.value?.models || []));
const requiresVerifyModel = computed(
() => form.validateAvailability && !isAgentAccountVerificationSkipped(form.provider),
);
const verifyModelOptions = computed(() => (form.id ? editModels.value : defaultModels.value));
const showVerifyModelSelect = computed(
() =>
!isAgentAccountVerificationSkipped(form.provider) &&
requiresVerifyModel.value &&
verifyModelOptions.value.length > 0 &&
(Boolean(form.id) || (!showModelDiscovery.value && !showInitialModel.value)),
);
@@ -255,6 +341,26 @@ const apiTypeURLHints: Record<string, { requestPath: string; example: string; en
example: 'http://127.0.0.1:8000',
endpointSuffixes: ['/v1/messages', '/messages'],
},
'openai-images': {
requestPath: '/images/generations',
example: 'http://127.0.0.1:8000/v1',
endpointSuffixes: ['/v1/images/generations', '/images/generations'],
},
'dashscope-images': {
requestPath: '/api/v1/services/aigc/multimodal-generation/generation',
example: 'https://dashscope.aliyuncs.com',
endpointSuffixes: ['/api/v1/services/aigc/multimodal-generation/generation'],
},
'minimax-images': {
requestPath: '/v1/image_generation',
example: 'https://api.minimaxi.com',
endpointSuffixes: ['/v1/image_generation'],
},
'openrouter-images': {
requestPath: '/api/v1/images',
example: 'https://openrouter.ai',
endpointSuffixes: ['/api/v1/images'],
},
};
const showAPITypeBaseURLTips = computed(() => Boolean(selectedAPIConfig.value?.editableBaseUrl));
const apiTypeBaseURLHelper = computed(() => {
@@ -262,6 +368,11 @@ const apiTypeBaseURLHelper = computed(() => {
if (!showAPITypeBaseURLTips.value || !hint) {
return '';
}
if (form.provider === 'custom' && isImageAPIType.value) {
return i18n.global.t('aiTools.agents.customImageURLHelper', [
`${hint.example.replace(/\/+$/, '')}${hint.requestPath}`,
]);
}
return i18n.global.t('aiTools.agents.apiTypeBaseURLHelper', [hint.requestPath, hint.example]);
});
@@ -330,23 +441,74 @@ const normalizeInitialModel = () => {
};
const resetInitialModel = () => {
form.initialModel = isInitialModelProvider(form.provider) ? buildInitialModel() : ({} as AI.AgentAccountModel);
form.initialModel =
isInitialModelProvider(form.provider) || isImageAPIType.value
? buildInitialModel()
: ({} as AI.AgentAccountModel);
};
const resetManualModels = () => {
manualModels.value = [buildInitialModel()];
manualVerifyIndex.value = -1;
};
const loadDefaultModels = () => {
discoveredModels.value = defaultModels.value.map((item) => ({
recordId: 0,
id: item.id,
name: item.name,
}));
modelDiscoveryFailed.value = false;
form.verifyModel = '';
};
const resetModelDiscovery = () => {
discovering.value = false;
discoveredModels.value = [];
modelConfigMode.value = 'discover';
modelDiscoveryFailed.value = false;
form.verifyModel = '';
resetManualModels();
loadDefaultModels();
};
const handleModelConfigModeChange = () => {
modelDiscoveryFailed.value = false;
form.verifyModel = '';
if (modelConfigMode.value === 'manual') {
discoveredModels.value = [];
};
const addManualModel = () => {
manualModels.value.push(buildInitialModel());
};
const removeManualModel = (index: number) => {
if (manualModels.value.length === 1) {
return;
}
manualModels.value.splice(index, 1);
if (manualVerifyIndex.value === index) {
manualVerifyIndex.value = -1;
} else if (manualVerifyIndex.value > index) {
manualVerifyIndex.value -= 1;
}
};
const normalizeManualModels = () => {
const models = manualModels.value.map((item) => ({
recordId: 0,
id: String(item.id || '').trim(),
name: String(item.name || '').trim(),
}));
if (models.some((item) => !item.id)) {
MsgError(i18n.global.t('aiTools.agents.accountModelsRequired'));
return null;
}
if (models.some((item) => item.id.includes(' '))) {
MsgError(i18n.global.t('setting.noSpace'));
return null;
}
if (new Set(models.map((item) => item.id)).size !== models.length) {
MsgError(i18n.global.t('aiTools.agents.accountModelsDuplicate'));
return null;
}
return models;
};
const discoverModels = async () => {
@@ -369,14 +531,23 @@ const discoverModels = async () => {
discoveredModels.value = res.data || [];
form.verifyModel = '';
if (discoveredModels.value.length === 0) {
modelConfigMode.value = 'manual';
if (defaultModels.value.length > 0) {
loadDefaultModels();
} else {
modelConfigMode.value = 'manual';
}
modelDiscoveryFailed.value = true;
} else {
modelDiscoveryFailed.value = false;
}
} catch (error: any) {
discoveredModels.value = [];
modelConfigMode.value = 'manual';
if (defaultModels.value.length > 0) {
loadDefaultModels();
modelDiscoveryFailed.value = true;
} else {
discoveredModels.value = [];
modelConfigMode.value = 'manual';
}
modelDiscoveryFailed.value = true;
MsgError(String(error?.message || i18n.global.t('commons.res.commonError')));
} finally {
@@ -394,12 +565,32 @@ const submit = async () => {
MsgError(i18n.global.t('aiTools.agents.accountModelsRequired'));
return;
}
if (showModelDiscovery.value && modelConfigMode.value === 'discover' && discoveredModels.value.length === 0) {
MsgError(i18n.global.t('aiTools.agents.accountModelsRequired'));
return;
let accountModels = initialModel ? [initialModel] : [];
if (!form.id) {
if (showModelDiscovery.value && modelConfigMode.value === 'manual') {
const models = normalizeManualModels();
if (!models) {
return;
}
accountModels = models;
} else if (showModelDiscovery.value) {
accountModels = discoveredModels.value;
}
if (accountModels.length === 0) {
MsgError(i18n.global.t('aiTools.agents.accountModelsRequired'));
return;
}
}
const verifyModel = showInitialModel.value ? initialModel?.id || '' : form.verifyModel;
if (!isAgentAccountVerificationSkipped(form.provider) && !verifyModel) {
let verifyModel = '';
if (requiresVerifyModel.value) {
verifyModel = form.verifyModel;
if (modelConfigMode.value === 'manual' && showModelDiscovery.value) {
verifyModel = accountModels[manualVerifyIndex.value]?.id || '';
} else if (showInitialModel.value) {
verifyModel = initialModel?.id || '';
}
}
if (requiresVerifyModel.value && !verifyModel) {
MsgError(i18n.global.t('aiTools.agents.verifyModelRequired'));
return;
}
@@ -415,6 +606,7 @@ const submit = async () => {
apiType: form.apiType,
authMode: form.authMode,
verifyModel,
validateAvailability: form.validateAvailability,
remark: form.remark,
syncAgents: form.syncAgents,
});
@@ -428,7 +620,8 @@ const submit = async () => {
apiType: form.apiType,
authMode: form.authMode,
verifyModel,
models: discoveredModels.value.length ? discoveredModels.value : initialModel ? [initialModel] : [],
validateAvailability: form.validateAvailability,
models: accountModels,
remark: form.remark,
});
}
@@ -452,6 +645,7 @@ const handleClose = () => {
form.authMode = '';
form.apiKey = '';
form.rememberApiKey = false;
form.validateAvailability = true;
form.verifyModel = '';
form.initialModel = {} as AI.AgentAccountModel;
editModels.value = [];
@@ -487,6 +681,7 @@ const openDrawer = async (params?: OpenParams) => {
form.baseURL = params.baseURL || '';
form.apiKey = params.apiKey || '';
form.rememberApiKey = params.rememberApiKey || false;
form.validateAvailability = true;
editModels.value = params.models || [];
const provider = providers.value[form.provider];
form.apiType = provider?.apiTypes.some((item) => item.apiType === params.apiType)
@@ -503,6 +698,7 @@ const openDrawer = async (params?: OpenParams) => {
form.baseURL = '';
form.apiKey = '';
form.rememberApiKey = false;
form.validateAvailability = true;
form.verifyModel = '';
editModels.value = [];
form.apiType = 'openai-completions';
@@ -536,7 +732,6 @@ const loadProviders = async () => {
};
const handleProviderChange = () => {
resetModelDiscovery();
const provider = selectedProvider.value;
form.apiType = provider?.defaultApiType || '';
const config = provider?.apiTypes.find((item) => item.apiType === form.apiType);
@@ -545,10 +740,10 @@ const handleProviderChange = () => {
if (!form.id) {
resetInitialModel();
}
resetModelDiscovery();
};
const handleAPITypeChange = () => {
resetModelDiscovery();
const config = selectedAPIConfig.value;
if (!config) {
return;
@@ -559,12 +754,12 @@ const handleAPITypeChange = () => {
form.baseURL = config.baseUrl || '';
}
resetInitialModel();
resetModelDiscovery();
};
watch([() => form.apiKey, () => form.baseURL], () => {
if (showModelDiscovery.value) {
discoveredModels.value = [];
modelDiscoveryFailed.value = false;
loadDefaultModels();
}
});
@@ -615,4 +810,8 @@ defineExpose({
.model-discovery .el-alert {
margin-bottom: 12px;
}
.model-discovery__add {
margin-top: 12px;
}
</style>
+41 -24
View File
@@ -7,6 +7,18 @@
</el-button>
</template>
<template #rightToolBar>
<el-select
v-model="apiType"
clearable
filterable
class="api-type-filter"
:placeholder="'API ' + $t('commons.table.type')"
@change="handleAPITypeChange"
>
<el-option v-for="item in apiTypeOptions" :key="item" :label="item" :value="item">
<ApiTypeTag :api-type="item" />
</el-option>
</el-select>
<TableSearch v-model:searchName="searchName" @search="search" />
<TableRefresh @search="search" />
</template>
@@ -15,13 +27,12 @@
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="200" />
<el-table-column :label="$t('aiTools.agents.provider')" prop="provider" width="200">
<template #default="{ row }">
<div class="provider-cell">
<ProviderLogo
:provider="row.provider"
:display-name="getAgentProviderDisplayName(row.provider, row.providerName)"
/>
<span>{{ getAgentProviderDisplayName(row.provider, row.providerName) }}</span>
</div>
<ProviderLabel :provider="row.provider" :display-name="row.providerName" />
</template>
</el-table-column>
<el-table-column :label="'API ' + $t('commons.table.type')" prop="apiType" min-width="170">
<template #default="{ row }">
<ApiTypeTag :api-type="row.apiType" />
</template>
</el-table-column>
<el-table-column label="Base URL" prop="baseUrl" min-width="200" />
@@ -59,20 +70,23 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { deleteAgentAccount, pageAgentAccounts } from '@/api/modules/ai';
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import AddDialog from '@/views/ai/agents/model/add/index.vue';
import ModelPoolDialog from '@/views/ai/agents/model/pool/index.vue';
import ProviderLogo from '@/components/agent-provider-logo/index.vue';
import ProviderLabel from '@/components/agent-provider-label/index.vue';
import ApiTypeTag from '@/components/api-type-tag/index.vue';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { dateFormat } from '@/utils/date';
import { getAgentProviderDisplayName, isAgentAccountVerificationSkipped } from '@/utils/agent';
import { isAgentAccountVerificationSkipped } from '@/utils/agent';
const items = ref<AI.AgentAccountItem[]>([]);
const addRef = ref();
const modelPoolRef = ref();
const searchName = ref('');
const apiType = ref('');
const apiTypeOptions = ref<string[]>([]);
const buttons = [
{
@@ -102,6 +116,7 @@ const search = async () => {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
provider: '',
apiType: apiType.value,
name: searchName.value || '',
};
const res = await pageAgentAccounts(req);
@@ -109,6 +124,18 @@ const search = async () => {
paginationConfig.total = res.data.total || 0;
};
const handleAPITypeChange = () => {
paginationConfig.currentPage = 1;
search();
};
const loadAPITypes = async () => {
const res = await getAgentProviders();
apiTypeOptions.value = Array.from(
new Set((res.data || []).flatMap((provider) => provider.apiTypes.map((item) => item.apiType))),
).sort();
};
const openCreate = () => {
if (addRef.value?.open) {
addRef.value.open();
@@ -177,22 +204,12 @@ const onDelete = async (row: AI.AgentAccountItem) => {
};
onMounted(async () => {
await search();
await Promise.all([search(), loadAPITypes()]);
});
</script>
<style scoped lang="scss">
.provider-cell {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
white-space: nowrap;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
<style scoped>
.api-type-filter {
width: 220px;
}
</style>
@@ -657,7 +657,7 @@ const loadFileAiManageSettings = async () => {
};
const loadFileAiAgentAccounts = async () => {
const res = await pageAgentAccounts({ page: 1, pageSize: 1000, provider: '', name: '' });
const res = await pageAgentAccounts({ page: 1, pageSize: 1000, provider: '', textOnly: true, name: '' });
fileAiAgentAccountOptions.value = res.data?.items || [];
};
@@ -145,6 +145,7 @@ const loadAgentAccounts = async () => {
page: 1,
pageSize: 1000,
provider: '',
textOnly: true,
name: '',
}).then((res) => {
agentAccountOptions.value = res.data?.items || [];