diff --git a/.github/screenshots/model-selector-custom.png b/.github/screenshots/model-selector-custom.png
new file mode 100644
index 0000000..bd2b4b8
Binary files /dev/null and b/.github/screenshots/model-selector-custom.png differ
diff --git a/packages/client/src/components/layout/ModelSelector.vue b/packages/client/src/components/layout/ModelSelector.vue
index 91c5ee3..f130c74 100644
--- a/packages/client/src/components/layout/ModelSelector.vue
+++ b/packages/client/src/components/layout/ModelSelector.vue
@@ -1,6 +1,6 @@
@@ -89,6 +126,7 @@ function openModal() {
@click="handleSelect(model, group.provider)"
>
{{ model }}
+ {{ t('models.customBadge') }}
@@ -98,6 +136,26 @@ function openModal() {
{{ searchQuery ? 'No results' : 'No models' }}
+
+
+
+
+
+
+ {{ t('models.customModelHint') }}
+
+
@@ -243,10 +301,48 @@ function openModal() {
color: $accent-primary;
}
+.model-badge-custom {
+ flex-shrink: 0;
+ font-size: 9px;
+ font-weight: 600;
+ color: #fff;
+ background: $accent-primary;
+ padding: 1px 5px;
+ border-radius: 3px;
+ margin-right: 4px;
+ letter-spacing: 0.03em;
+}
+
.model-empty {
padding: 24px 0;
text-align: center;
font-size: 13px;
color: $text-muted;
}
+
+.model-custom {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid $border-color;
+}
+
+.model-custom-row {
+ display: flex;
+ gap: 8px;
+}
+
+.model-custom-provider {
+ width: 160px;
+ flex-shrink: 0;
+}
+
+.model-custom-input {
+ flex: 1;
+}
+
+.model-custom-hint {
+ margin-top: 6px;
+ font-size: 11px;
+ color: $text-muted;
+}
diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts
index a011e98..8abc87d 100644
--- a/packages/client/src/i18n/locales/de.ts
+++ b/packages/client/src/i18n/locales/de.ts
@@ -252,6 +252,9 @@ export default {
nousApproved: 'Login erfolgreich',
nousDenied: 'Autorisierung wurde abgelehnt',
nousExpired: 'Autorisierung abgelaufen',
+ customBadge: 'BENUTZERDEF.',
+ customModelPlaceholder: 'Benutzerdefinierter Modellname',
+ customModelHint: 'Enter zum Laden',
noProviders: 'Keine Anbieter gefunden. Fugen Sie einen benutzerdefinierten Anbieter hinzu, um zu beginnen.',
builtIn: 'Integriert',
customType: 'Benutzerdefiniert',
diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts
index 72702ba..7e1f190 100644
--- a/packages/client/src/i18n/locales/en.ts
+++ b/packages/client/src/i18n/locales/en.ts
@@ -277,6 +277,9 @@ export default {
nousApproved: 'Login successful',
nousDenied: 'Authorization was denied. Please try again.',
nousExpired: 'Authorization expired. Please try again.',
+ customBadge: 'CUSTOM',
+ customModelPlaceholder: 'Custom model name',
+ customModelHint: 'Enter to load',
noProviders: 'No providers found. Add a custom provider to get started.',
builtIn: 'Built-in',
customType: 'Custom',
diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts
index ab255d0..66b9442 100644
--- a/packages/client/src/i18n/locales/es.ts
+++ b/packages/client/src/i18n/locales/es.ts
@@ -252,6 +252,9 @@ export default {
nousApproved: 'Inicio de sesión exitoso',
nousDenied: 'Autorización denegada',
nousExpired: 'Autorización expirada',
+ customBadge: 'PERSONALIZADO',
+ customModelPlaceholder: 'Nombre del modelo personalizado',
+ customModelHint: 'Enter para cargar',
noProviders: 'No se encontraron proveedores. Anade un proveedor personalizado para comenzar.',
builtIn: 'Integrado',
customType: 'Personalizado',
diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts
index 56dcc0e..ca1dd66 100644
--- a/packages/client/src/i18n/locales/fr.ts
+++ b/packages/client/src/i18n/locales/fr.ts
@@ -252,6 +252,9 @@ export default {
nousApproved: 'Connexion réussie',
nousDenied: 'Autorisation refusée',
nousExpired: 'Autorisation expirée',
+ customBadge: 'PERSONNALISÉ',
+ customModelPlaceholder: 'Nom du modèle personnalisé',
+ customModelHint: 'Entrée pour charger',
noProviders: 'Aucun fournisseur trouve. Ajoutez un fournisseur personnalise pour commencer.',
builtIn: 'Integre',
customType: 'Personnalise',
diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts
index 6965b91..cb6b002 100644
--- a/packages/client/src/i18n/locales/ja.ts
+++ b/packages/client/src/i18n/locales/ja.ts
@@ -252,6 +252,9 @@ export default {
nousApproved: 'ログイン成功',
nousDenied: '認証が拒否されました',
nousExpired: '認証の有効期限が切れました',
+ customBadge: 'カスタム',
+ customModelPlaceholder: 'カスタムモデル名',
+ customModelHint: 'Enterで読み込み',
noProviders: 'プロバイダーがありません。カスタムプロバイダーを追加して始めましょう。',
builtIn: '組み込み',
customType: 'カスタム',
diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts
index d3d2141..6a84bbb 100644
--- a/packages/client/src/i18n/locales/ko.ts
+++ b/packages/client/src/i18n/locales/ko.ts
@@ -252,6 +252,9 @@ export default {
nousApproved: '로그인 성공',
nousDenied: '인증이 거부되었습니다',
nousExpired: '인증이 만료되었습니다',
+ customBadge: '커스텀',
+ customModelPlaceholder: '사용자 지정 모델 이름',
+ customModelHint: 'Enter로 불러오기',
noProviders: 'Provider가 없습니다. 사용자 지정 Provider를 추가하여 시작하세요.',
builtIn: '내장',
customType: '사용자 지정',
diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts
index faa3905..ec1b382 100644
--- a/packages/client/src/i18n/locales/pt.ts
+++ b/packages/client/src/i18n/locales/pt.ts
@@ -252,6 +252,9 @@ export default {
nousApproved: 'Login bem-sucedido',
nousDenied: 'Autorização negada',
nousExpired: 'Autorização expirada',
+ customBadge: 'PERSONALIZADO',
+ customModelPlaceholder: 'Nome do modelo personalizado',
+ customModelHint: 'Enter para carregar',
noProviders: 'Nenhum provedor encontrado. Adicione um provedor personalizado para comecar.',
builtIn: 'Integrado',
customType: 'Personalizado',
diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts
index 9dcadc5..9b1ae51 100644
--- a/packages/client/src/i18n/locales/zh.ts
+++ b/packages/client/src/i18n/locales/zh.ts
@@ -277,6 +277,9 @@ export default {
nousApproved: '登录成功',
nousDenied: '授权被拒绝,请重试。',
nousExpired: '授权已过期,请重试。',
+ customBadge: '自定义',
+ customModelPlaceholder: '自定义模型名称',
+ customModelHint: '按回车加载',
noProviders: '暂无 Provider,添加一个开始吧。',
builtIn: '内置',
customType: '自定义',
diff --git a/packages/client/src/stores/hermes/app.ts b/packages/client/src/stores/hermes/app.ts
index 70d85f6..5df84a2 100644
--- a/packages/client/src/stores/hermes/app.ts
+++ b/packages/client/src/stores/hermes/app.ts
@@ -19,6 +19,7 @@ export const useAppStore = defineStore('app', () => {
const modelGroups = ref([])
const selectedModel = ref('')
const selectedProvider = ref('')
+ const customModels = ref>({})
const healthPollTimer = ref>()
const nodeVersion = ref('')
@@ -73,6 +74,13 @@ export const useAppStore = defineStore('app', () => {
await updateDefaultModel({ default: modelId, provider })
selectedModel.value = modelId
selectedProvider.value = provider || ''
+ // Track as custom if not already in the server-fetched list
+ if (provider && !modelGroups.value.find(g => g.provider === provider)?.models.includes(modelId)) {
+ if (!customModels.value[provider]) customModels.value[provider] = []
+ if (!customModels.value[provider].includes(modelId)) {
+ customModels.value[provider] = [...customModels.value[provider], modelId]
+ }
+ }
} catch (err: any) {
console.error('Failed to switch model:', err)
}
@@ -122,6 +130,7 @@ export const useAppStore = defineStore('app', () => {
updating,
doUpdate,
modelGroups,
+ customModels,
selectedModel,
selectedProvider,
streamEnabled,
diff --git a/packages/server/src/controllers/hermes/models.ts b/packages/server/src/controllers/hermes/models.ts
index a83d7a0..164c179 100644
--- a/packages/server/src/controllers/hermes/models.ts
+++ b/packages/server/src/controllers/hermes/models.ts
@@ -83,7 +83,7 @@ export async function getAvailable(ctx: any) {
const builtinPreset = PROVIDER_PRESETS.find(p => p.value === bareKey)
let models = builtinPreset?.models?.length ? [...builtinPreset.models] : [cp.model]
if (cp.api_key) {
- try { const fetched = await fetchProviderModels(baseUrl, cp.api_key); if (fetched.length > 0) models = fetched } catch { }
+ try { const fetched = await fetchProviderModels(baseUrl, cp.api_key); if (fetched.length > 0) models = [...new Set([cp.model, ...fetched])] } catch { }
}
const label = builtinPreset?.label || cp.name
const presetBaseUrl = builtinPreset?.base_url || ''