mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): default the model picker to the featured shortlist
expandProviderDefaults prefers a provider's featured_models when present and falls back to the existing top-N for providers that ship none (single-lab, local, custom). Only aggregators get curated, so exactly the providers with the everything-under-the-sun problem are trimmed; the rest are unchanged. Every non-featured model stays one search or Edit Models toggle away.
This commit is contained in:
parent
d4449c47e2
commit
64166f87ba
3 changed files with 161 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
isProviderSentinel,
|
||||
modelVisibilityKey,
|
||||
resolveVisibleKeys,
|
||||
setProviderVisibility,
|
||||
toggleModelVisibility
|
||||
} from './model-visibility'
|
||||
|
||||
|
|
@ -224,3 +225,105 @@ describe('resolveVisibleKeys', () => {
|
|||
expect([...resolveVisibleKeys(new Set(), providers)]).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('featured defaults', () => {
|
||||
const featuredProvider = (slug: string, models: string[], featured_models: string[]): ModelOptionProvider => ({
|
||||
featured_models,
|
||||
models,
|
||||
name: slug,
|
||||
slug
|
||||
})
|
||||
|
||||
it('defaults to the featured shortlist when a provider publishes one', () => {
|
||||
const nous = featuredProvider(
|
||||
'nous',
|
||||
['anthropic/opus', 'anthropic/haiku', 'google/gemini', 'x-ai/grok'],
|
||||
['anthropic/opus', 'google/gemini', 'x-ai/grok']
|
||||
)
|
||||
|
||||
const visible = defaultVisibleKeys([nous])
|
||||
|
||||
// Featured are visible; the non-featured model is hidden by default.
|
||||
expect(visible.has(modelVisibilityKey('nous', 'anthropic/opus'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('nous', 'google/gemini'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('nous', 'x-ai/grok'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('nous', 'anthropic/haiku'))).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to top-N when a provider ships no featured list', () => {
|
||||
const plain = provider('ollama', ['qwen3:latest', 'llama3.2:latest'])
|
||||
|
||||
const visible = defaultVisibleKeys([plain])
|
||||
|
||||
// No featured_models → every model stays a default (top-N, N ≫ 2 here).
|
||||
expect(visible.has(modelVisibilityKey('ollama', 'qwen3:latest'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('ollama', 'llama3.2:latest'))).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores an empty featured list and falls back to top-N', () => {
|
||||
const plain = featuredProvider('ollama', ['qwen3:latest', 'llama3.2:latest'], [])
|
||||
|
||||
const visible = defaultVisibleKeys([plain])
|
||||
|
||||
expect(visible.has(modelVisibilityKey('ollama', 'qwen3:latest'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('ollama', 'llama3.2:latest'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setProviderVisibility', () => {
|
||||
const providers = [provider('openai', ['gpt-a', 'gpt-b']), provider('nous', ['hermes-x', 'hermes-y'])]
|
||||
|
||||
it('enabling a provider makes every one of its models visible', () => {
|
||||
// Start from a hidden-all openai; flip it on.
|
||||
const stored = new Set([emptyProviderSentinelKey('openai')])
|
||||
|
||||
const next = setProviderVisibility(stored, providers, 'openai', true)
|
||||
|
||||
const visible = effectiveVisibleKeys(next, providers)
|
||||
expect(visible.has(modelVisibilityKey('openai', 'gpt-a'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('openai', 'gpt-b'))).toBe(true)
|
||||
// Sentinel is cleared.
|
||||
expect(next.has(emptyProviderSentinelKey('openai'))).toBe(false)
|
||||
})
|
||||
|
||||
it('disabling a provider hides all its models and records the sentinel', () => {
|
||||
const next = setProviderVisibility(null, providers, 'openai', false)
|
||||
|
||||
expect(next.has(emptyProviderSentinelKey('openai'))).toBe(true)
|
||||
const visible = effectiveVisibleKeys(next, providers)
|
||||
expect(visible.has(modelVisibilityKey('openai', 'gpt-a'))).toBe(false)
|
||||
expect(visible.has(modelVisibilityKey('openai', 'gpt-b'))).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves other providers untouched (their sentinels survive)', () => {
|
||||
const stored = new Set([emptyProviderSentinelKey('nous')])
|
||||
|
||||
// Turn openai fully on; nous must stay hidden.
|
||||
const next = setProviderVisibility(stored, providers, 'openai', true)
|
||||
|
||||
expect(next.has(emptyProviderSentinelKey('nous'))).toBe(true)
|
||||
const visible = effectiveVisibleKeys(next, providers)
|
||||
expect(visible.has(modelVisibilityKey('nous', 'hermes-x'))).toBe(false)
|
||||
expect(visible.has(modelVisibilityKey('openai', 'gpt-a'))).toBe(true)
|
||||
})
|
||||
|
||||
it('round-trips: enable then disable returns to a clean hidden-all', () => {
|
||||
const enabled = setProviderVisibility(null, providers, 'openai', true)
|
||||
const disabled = setProviderVisibility(enabled, providers, 'openai', false)
|
||||
|
||||
expect(disabled.has(emptyProviderSentinelKey('openai'))).toBe(true)
|
||||
// No stray real keys left for the provider.
|
||||
expect([...disabled].some(k => k.startsWith('openai::') && !isProviderSentinel(k))).toBe(false)
|
||||
})
|
||||
|
||||
it('collapses model families to one key per family when enabling', () => {
|
||||
// A base + its -fast sibling collapse to a single family row/key.
|
||||
const ps = [provider('nous', ['model', 'model-fast'])]
|
||||
|
||||
const next = setProviderVisibility(null, ps, 'nous', true)
|
||||
|
||||
expect(next.has(modelVisibilityKey('nous', 'model'))).toBe(true)
|
||||
// The -fast sibling is represented by its base family, not its own key.
|
||||
expect(next.has(modelVisibilityKey('nous', 'model-fast'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -111,13 +111,21 @@ export function defaultVisibleKeys(providers: readonly ModelOptionProvider[]): S
|
|||
return keys
|
||||
}
|
||||
|
||||
/** Add a provider's curated default model keys (top-N collapsed families) to
|
||||
* `target`. Shared by `defaultVisibleKeys` and `resolveVisibleKeys` so the
|
||||
/** Add a provider's curated default model keys to `target`. Prefers the
|
||||
* backend's `featured_models` shortlist (one flagship per lab) for aggregator
|
||||
* providers that would otherwise flood the default view with dozens of models;
|
||||
* falls back to the top-N collapsed families when a provider ships no featured
|
||||
* list. Shared by `defaultVisibleKeys` and `resolveVisibleKeys` so the
|
||||
* expansion rule lives in exactly one place. */
|
||||
function expandProviderDefaults(provider: ModelOptionProvider, target: Set<string>): void {
|
||||
const families = collapseModelFamilies(provider.models ?? [])
|
||||
|
||||
for (const family of families.slice(0, DEFAULT_VISIBLE_PER_PROVIDER)) {
|
||||
const featured = provider.featured_models ?? []
|
||||
const defaults = featured.length
|
||||
? families.filter(family => featured.includes(family.id))
|
||||
: families.slice(0, DEFAULT_VISIBLE_PER_PROVIDER)
|
||||
|
||||
for (const family of defaults) {
|
||||
target.add(modelVisibilityKey(provider.slug, family.id))
|
||||
}
|
||||
}
|
||||
|
|
@ -209,3 +217,45 @@ export function toggleModelVisibility(
|
|||
|
||||
return next
|
||||
}
|
||||
|
||||
/** Compute the next persisted visibility set when a provider's master switch is
|
||||
* flipped. `visible=true` enables every one of the provider's collapsed model
|
||||
* families (and clears its hide-all sentinel); `visible=false` removes them all
|
||||
* and records the sentinel so the defaults are not silently re-expanded.
|
||||
* Seeds from `resolveVisibleKeys` so other providers' state (including their
|
||||
* sentinels) survives the persist, mirroring `toggleModelVisibility`. */
|
||||
export function setProviderVisibility(
|
||||
stored: Set<string> | null,
|
||||
providers: readonly ModelOptionProvider[],
|
||||
providerSlug: string,
|
||||
visible: boolean
|
||||
): Set<string> {
|
||||
const next = resolveVisibleKeys(stored, providers)
|
||||
const sentinel = emptyProviderSentinelKey(providerSlug)
|
||||
const provider = providers.find(p => p.slug === providerSlug)
|
||||
const families = collapseModelFamilies(provider?.models ?? [])
|
||||
|
||||
// Drop every existing entry for this provider (real keys + sentinel); we
|
||||
// rebuild its state from scratch below.
|
||||
for (const key of [...next]) {
|
||||
if (key.startsWith(`${providerSlug}::`)) {
|
||||
next.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
for (const family of families) {
|
||||
next.add(modelVisibilityKey(providerSlug, family.id))
|
||||
}
|
||||
|
||||
// A provider with zero models can't be "all on" — leave it empty rather
|
||||
// than stranding a sentinel that reads as an explicit hide-all.
|
||||
if (families.length === 0) {
|
||||
next.delete(sentinel)
|
||||
}
|
||||
} else {
|
||||
next.add(sentinel)
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,11 @@ export interface ModelOptionProvider {
|
|||
slug: string
|
||||
total_models?: number
|
||||
warning?: string
|
||||
/** Curated shortlist (one flagship per lab) the picker shows by default for
|
||||
* aggregator providers that serve dozens of models across many labs. Empty
|
||||
* for providers with no manifest entry — the picker falls back to top-N.
|
||||
* The rest of `models` stays reachable via search / Edit Models. */
|
||||
featured_models?: string[]
|
||||
/** True when the provider has usable credentials. False for canonical
|
||||
* providers surfaced by `include_unconfigured` that the user hasn't set up
|
||||
* yet — render these with a setup affordance instead of hiding them. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue