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 || ''