From 3c04527e1e79354dae54600a9c8129b67deb86bd Mon Sep 17 00:00:00 2001 From: emozilla Date: Sun, 31 May 2026 05:57:28 -0400 Subject: [PATCH] feat(desktop): show model pricing + free/paid tier gating in GUI picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI `hermes model` picker shows per-model $/Mtok pricing and gates paid models on free Nous accounts. The GUI picker showed bare model names. Bring it to parity across both the model-picker dialog and onboarding confirm card. Backend: - inventory.build_models_payload gains a pricing=True flag → _apply_pricing enriches each provider row with formatted per-model pricing ({input,output,cache,free}) via the same _format_price_per_mtok the CLI uses, and for Nous adds free_tier + unavailable_models (paid models a free user can't select) via check_nous_free_tier + partition_nous_models_by_tier. Best-effort: any pricing/tier failure is swallowed and fails open (no gating). - /api/model/options and TUI model.options now pass pricing=True so the global picker and in-session picker both carry pricing. Frontend: - ModelOptionProvider gains pricing/free_tier/unavailable_models; new ModelPricing type. - model-picker dialog renders In/Out $/Mtok (or a Free pill) per model, a Free tier/Pro badge on the Nous heading, and disables + grays unavailable paid models for free users with a 'Pro models need a paid subscription' note. - onboarding confirm card shows the chosen model's price + tier badge. Tests: test_inventory_pricing covers price formatting, free-tier gating, paid no-gating, providers without pricing, and swallowed failures. --- .../components/desktop-onboarding-overlay.tsx | 33 ++++++- apps/desktop/src/components/model-picker.tsx | 71 +++++++++++++- apps/desktop/src/types/hermes.ts | 18 ++++ hermes_cli/inventory.py | 90 +++++++++++++++++ hermes_cli/web_server.py | 2 +- tests/hermes_cli/test_inventory_pricing.py | 98 +++++++++++++++++++ tui_gateway/server.py | 1 + 7 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 tests/hermes_cli/test_inventory_pricing.py diff --git a/apps/desktop/src/components/desktop-onboarding-overlay.tsx b/apps/desktop/src/components/desktop-onboarding-overlay.tsx index d3f12a71dff..f3eb8fc1a51 100644 --- a/apps/desktop/src/components/desktop-onboarding-overlay.tsx +++ b/apps/desktop/src/components/desktop-onboarding-overlay.tsx @@ -1,9 +1,11 @@ import { useStore } from '@nanostores/react' +import { useQuery } from '@tanstack/react-query' import { useEffect, useMemo, useRef, useState } from 'react' import { ModelPickerDialog } from '@/components/model-picker' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { getGlobalModelOptions } from '@/hermes' import { Check, ChevronDown, @@ -660,6 +662,18 @@ function ConfirmingModelPanel({ // a familiar UI for users who'll see this picker again later. const [pickerOpen, setPickerOpen] = useState(false) + // Pull pricing + tier for the just-picked default so the confirm card + // shows the same $/Mtok + Free/Pro info the picker and CLI do. + const options = useQuery({ + queryKey: ['onboarding-model-options', flow.providerSlug], + queryFn: () => getGlobalModelOptions() + }) + const providerRow = options.data?.providers?.find( + p => String(p.slug).toLowerCase() === flow.providerSlug.toLowerCase() + ) + const price = providerRow?.pricing?.[flow.currentModel] + const freeTier = providerRow?.free_tier + return (
@@ -670,8 +684,25 @@ function ConfirmingModelPanel({
-

Default model

+
+

Default model

+ {freeTier === true && ( + + Free tier + + )} + {freeTier === false && ( + + Pro + + )} +

{flow.currentModel}

+ {price && (price.input || price.output) && ( +

+ {price.free ? 'Free' : `${price.input || '?'} in / ${price.output || '?'} out per Mtok`} +

+ )}