feat(desktop): mirror hermes model free/paid curation in GUI onboarding
GUI onboarding picked models[0] from /api/model/options, which ignores the
Nous free/paid tier — a free user could land on a paid default (e.g.
anthropic/claude-opus-4). Now the recommended default mirrors what `hermes
model` does.
- web_server.py: new GET /api/model/recommended-default?provider=<slug>. For
Nous it runs the same curation as the CLI (get_curated_nous_model_ids +
pricing + check_nous_free_tier + union_with_portal_{free,paid}_recommendations
+ partition_nous_models_by_tier) so free users get a free model and paid users
get the curated default. Other providers fall back to the first curated model.
Never 500s — returns empty model on error so onboarding degrades gracefully.
- hermes.ts: getRecommendedDefaultModel client + RecommendedDefaultModel type.
- onboarding.ts: fetchProviderDefaultModel prefers the recommended endpoint,
falls back to models[0] when unavailable.
- tests: free-tier picks free model, paid-tier picks curated default, failure
returns empty without 500.
This commit is contained in:
@@ -449,6 +449,22 @@ export function getGlobalModelOptions(): Promise<ModelOptionsResponse> {
|
||||
})
|
||||
}
|
||||
|
||||
export interface RecommendedDefaultModel {
|
||||
provider: string
|
||||
model: string
|
||||
/** True/false for Nous (free vs paid tier); null for other providers. */
|
||||
free_tier: boolean | null
|
||||
}
|
||||
|
||||
// Recommended default model for a freshly-authenticated provider. Mirrors the
|
||||
// curation `hermes model` does — for Nous it honors the free/paid tier so a
|
||||
// free user gets a free model instead of a paid default.
|
||||
export function getRecommendedDefaultModel(provider: string): Promise<RecommendedDefaultModel> {
|
||||
return window.hermesDesktop.api<RecommendedDefaultModel>({
|
||||
path: `/api/model/recommended-default?provider=${encodeURIComponent(provider)}`
|
||||
})
|
||||
}
|
||||
|
||||
export function setGlobalModel(
|
||||
provider: string,
|
||||
model: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import { atom } from 'nanostores'
|
||||
import {
|
||||
cancelOAuthSession,
|
||||
getGlobalModelOptions,
|
||||
getRecommendedDefaultModel,
|
||||
listOAuthProviders,
|
||||
pollOAuthSession,
|
||||
setEnvVar,
|
||||
@@ -204,9 +205,28 @@ async function fetchProviderDefaultModel(
|
||||
return null
|
||||
}
|
||||
|
||||
// Prefer the backend's recommended default — it mirrors the curation
|
||||
// `hermes model` does (for Nous it honors the user's free/paid tier, so a
|
||||
// free user gets a free model rather than a paid default like opus). Fall
|
||||
// back to the first curated model if the endpoint can't resolve one.
|
||||
let defaultModel = String(models[0])
|
||||
try {
|
||||
const recommended = await getRecommendedDefaultModel(String(matched.slug))
|
||||
if (recommended.model && models.map(String).includes(recommended.model)) {
|
||||
defaultModel = recommended.model
|
||||
} else if (recommended.model) {
|
||||
// Recommended model isn't in the curated options list (e.g. a Portal
|
||||
// free-recommendation the picker list didn't include); trust it anyway.
|
||||
defaultModel = recommended.model
|
||||
}
|
||||
} catch {
|
||||
// Endpoint unavailable — keep models[0]. Non-fatal: the confirm card still
|
||||
// shows and the user can change it.
|
||||
}
|
||||
|
||||
return {
|
||||
providerSlug: String(matched.slug),
|
||||
defaultModel: String(models[0])
|
||||
defaultModel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user