Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Hermes Agent Environment Configuration
# Copy this file to .env and fill in your API keys

# =============================================================================
# LLM PROVIDER (Fireworks AI) — preferred BYOK provider
# =============================================================================
# Fireworks AI — fastest inference for production AI, 300+ models, one API key.
# Get your key at: https://app.fireworks.ai/settings/users/api-keys
# Address models directly by catalog ID, e.g.
# accounts/fireworks/models/kimi-k2p6, accounts/fireworks/models/glm-5p2
# FIREWORKS_API_KEY=
# FIREWORKS_BASE_URL=https://api.fireworks.ai/inference/v1 # Override default base URL

# =============================================================================
# LLM PROVIDER (OpenRouter)
# =============================================================================
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/public/fireworks-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/desktop/public/fireworks.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 18 additions & 4 deletions apps/desktop/src/components/onboarding/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ afterEach(() => {
})

describe('onboarding Picker', () => {
it('features Nous Portal and hides other providers behind a disclosure', () => {
it('features Nous Portal and Fireworks, and hides other providers behind a disclosure', () => {
setProviders([provider('anthropic', 'Anthropic Claude'), provider('nous', 'Nous Portal')])
render(<Picker ctx={ctx} />)

expect(screen.getByText('Nous Portal')).toBeTruthy()
expect(screen.getByText('Recommended')).toBeTruthy()
// Fireworks is the preferred BYOK provider — a second hero card after Nous.
expect(screen.getByText('Fireworks AI')).toBeTruthy()
// Both hero cards carry a Recommended badge; the plain rows below do not.
expect(screen.getAllByText('Recommended')).toHaveLength(2)
expect(screen.queryByText('Anthropic API Key')).toBeNull()

fireEvent.click(screen.getByRole('button', { name: 'Other providers' }))
Expand All @@ -70,14 +73,25 @@ describe('onboarding Picker', () => {
expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy()
})

it('shows every provider directly when Nous Portal is absent', () => {
it('still features Fireworks when Nous Portal is absent', () => {
setProviders([provider('anthropic', 'Anthropic Claude'), provider('openai-codex', 'OpenAI Codex / ChatGPT')])
render(<Picker ctx={ctx} />)

expect(screen.getByText('Anthropic API Key')).toBeTruthy()
expect(screen.getByText('OpenAI OAuth (ChatGPT)')).toBeTruthy()
expect(screen.queryByText('Other sign-in options')).toBeNull()
expect(screen.queryByText('Recommended')).toBeNull()
// Fireworks stays featured even without Nous — exactly one Recommended badge.
expect(screen.getByText('Fireworks AI')).toBeTruthy()
expect(screen.getAllByText('Recommended')).toHaveLength(1)
})

it('routes the Fireworks hero card into the API-key form', () => {
setProviders([provider('nous', 'Nous Portal')])
render(<Picker ctx={ctx} />)

fireEvent.click(screen.getByText('Fireworks AI'))

expect($desktopOnboarding.get().mode).toBe('apikey')
})

it('offers "choose later" on first run and persists the skip', () => {
Expand Down
34 changes: 29 additions & 5 deletions apps/desktop/src/components/onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ import {
import type { ModelOptionProvider, OAuthProvider } from '@/types/hermes'

import { DocsLink, FlowPanel, Status } from './flow'
import { FeaturedProviderRow, KeyProviderRow, ProviderRow, sortProviders } from './providers'
import { FeaturedFireworksRow, FeaturedProviderRow, KeyProviderRow, ProviderRow, sortProviders } from './providers'

export { FeaturedProviderRow, KeyProviderRow, ProviderRow, providerTitle, sortProviders } from './providers'
export {
FeaturedFireworksRow,
FeaturedProviderRow,
KeyProviderRow,
ProviderRow,
providerTitle,
sortProviders
} from './providers'

interface DesktopOnboardingOverlayProps {
enabled: boolean
Expand All @@ -49,6 +56,12 @@ export interface ApiKeyOption {
}

const API_KEY_OPTIONS: ApiKeyOption[] = [
{
id: 'fireworks',
name: 'Fireworks AI',
envKey: 'FIREWORKS_API_KEY',
docsUrl: 'https://app.fireworks.ai/settings/users/api-keys'
},
{
id: 'openrouter',
name: 'OpenRouter',
Expand Down Expand Up @@ -395,6 +408,14 @@ export function Picker({ ctx }: { ctx: OnboardingContext }) {
const { t } = useI18n()
const { localEndpoint, manual, mode, providers } = useStore($desktopOnboarding)
const [showAll, setShowAll] = useState(readShowAll)
// Which key-form option to preselect when we flip to 'apikey' mode. Set by the
// Fireworks hero card (its BYOK key) and the OpenRouter row; the generic
// "I have an API key" link leaves it undefined and lands on the first option.
const [apiKeyInitialEnv, setApiKeyInitialEnv] = useState<string | undefined>(undefined)
const openKeyForm = (envKey?: string) => {
setApiKeyInitialEnv(envKey)
setOnboardingMode('apikey')
}
const ordered = useMemo(() => (providers ? sortProviders(providers) : []), [providers])
const hasOauth = ordered.length > 0
const apiKeyOptions = useApiKeyCatalog()
Expand All @@ -408,7 +429,7 @@ export function Picker({ ctx }: { ctx: OnboardingContext }) {
<div className="grid gap-3">
<ApiKeyForm
canGoBack={hasOauth && !localEndpoint}
initialEnvKey={localEndpoint ? 'OPENAI_BASE_URL' : undefined}
initialEnvKey={localEndpoint ? 'OPENAI_BASE_URL' : apiKeyInitialEnv}
onBack={() => setOnboardingMode('oauth')}
onSave={(envKey, value, name, apiKey) => saveOnboardingApiKey(envKey, value, name, ctx, apiKey)}
options={apiKeyOptions}
Expand Down Expand Up @@ -438,12 +459,15 @@ export function Picker({ ctx }: { ctx: OnboardingContext }) {
<div className="grid gap-2">
<div className="grid max-h-[60dvh] gap-2 overflow-y-auto p-1">
{featured ? <FeaturedProviderRow onSelect={select} provider={featured} /> : null}
{/* Fireworks — preferred BYOK provider, second hero card after Nous.
Routes into the API-key form preselecting the Fireworks key. */}
<FeaturedFireworksRow onSelect={() => openKeyForm('FIREWORKS_API_KEY')} />
{showRest ? (
<>
{rest.map(p => (
<ProviderRow key={p.id} onSelect={select} provider={p} />
))}
<KeyProviderRow onClick={() => setOnboardingMode('apikey')} />
<KeyProviderRow onClick={() => openKeyForm('OPENROUTER_API_KEY')} />
</>
) : null}
</div>
Expand All @@ -466,7 +490,7 @@ export function Picker({ ctx }: { ctx: OnboardingContext }) {
{manual ? <span /> : <ChooseLaterLink />}
<Button
className="-mr-2 font-medium"
onClick={() => setOnboardingMode('apikey')}
onClick={() => openKeyForm()}
size="xs"
type="button"
variant="text"
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src/components/onboarding/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@ export function FeaturedProviderRow({
)
}

// Fireworks is the preferred BYOK provider — a second hero card directly below
// Nous Portal. It isn't an OAuth provider (no sign-in flow), so it takes a bare
// onSelect that routes into the API-key form rather than an OAuthProvider.
export function FeaturedFireworksRow({ onSelect }: { onSelect: () => void }) {
const { t } = useI18n()

return (
<button
className="group relative flex w-full items-center justify-between gap-4 rounded-[8px] bg-primary/[0.06] px-3 py-2.5 text-left transition-colors hover:bg-primary/10"
onClick={onSelect}
type="button"
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<img alt="" className="size-5 shrink-0" src={assetPath('fireworks.svg')} />
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">Fireworks AI</span>
<span className="inline-flex items-center gap-1.5 bg-[#6720FF] px-2 py-0.5 text-[0.64rem] font-semibold uppercase tracking-[0.16em] text-white">
<img alt="" aria-hidden="true" className="size-2.5 shrink-0" src={assetPath('fireworks-white.svg')} />
{t.onboarding.recommended}
</span>
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.fireworksPitch}</p>
</div>
<ChevronRight className="size-4 shrink-0 text-primary transition group-hover:translate-x-0.5" />
</button>
)
}

function ConnectedTag() {
const { t } = useI18n()

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1895,8 +1895,13 @@ export const en: Translations = {
recommended: 'Recommended',
connected: 'Connected',
featuredPitch: 'One subscription, 300+ frontier models — the recommended way to run Hermes',
fireworksPitch: 'Fastest inference for production AI — 300+ models, one API, deploy in a click',
openRouterPitch: 'One key, hundreds of models — a solid default',
apiKeyOptions: {
fireworks: {
short: 'fastest inference',
description: 'Direct access to 300+ open & proprietary models on Fireworks AI — one API key, production speed.'
},
openrouter: {
short: 'one key, many models',
description: 'Hosts hundreds of models behind a single key. Good default for new installs.'
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1873,8 +1873,13 @@ export const ja = defineLocale({
recommended: '推奨',
connected: '接続済み',
featuredPitch: '1 つのサブスクリプションで 300 以上の最先端モデル — Hermes を実行するための推奨方法',
fireworksPitch: 'プロダクション AI 向けの最速推論 — 300 以上のモデル、1 つの API、ワンクリックでデプロイ',
openRouterPitch: '1 つのキーで数百のモデル — 堅実なデフォルト',
apiKeyOptions: {
fireworks: {
short: '最速の推論',
description: 'Fireworks AI で 300 以上のオープン/プロプライエタリモデルに直接アクセス — 1 つの API キー、プロダクション速度。'
},
openrouter: {
short: '1 つのキーで多くのモデル',
description: '1 つのキーで数百のモデルをホスト。新規インストールのデフォルトとして最適。'
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,7 @@ export interface Translations {
recommended: string
connected: string
featuredPitch: string
fireworksPitch: string
openRouterPitch: string
apiKeyOptions: Record<string, { short: string; description: string }>
backToSignIn: string
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/i18n/zh-hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1816,8 +1816,10 @@ export const zhHant = defineLocale({
recommended: '建議',
connected: '已連線',
featuredPitch: '一個訂閱,300+ 前沿模型 — 執行 Hermes 的建議方式',
fireworksPitch: '面向生產 AI 的最快推理 — 300+ 模型,一個 API,一鍵部署',
openRouterPitch: '一個金鑰,數百個模型 — 穩定的預設選擇',
apiKeyOptions: {
fireworks: { short: '最快推理', description: '透過 Fireworks AI 直接存取 300+ 開源與專有模型 — 一個 API 金鑰,生產級速度。' },
openrouter: { short: '一個金鑰,多個模型', description: '用一個金鑰存取數百個模型。適合新安裝的預設選擇。' },
openai: { short: 'GPT 等級模型', description: '直接存取 OpenAI 模型。' },
gemini: { short: 'Gemini 模型', description: '直接存取 Google Gemini 模型。' },
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2067,8 +2067,10 @@ export const zh: Translations = {
recommended: '推荐',
connected: '已连接',
featuredPitch: '一个订阅,300+ 前沿模型 — 运行 Hermes 的推荐方式',
fireworksPitch: '面向生产 AI 的最快推理 — 300+ 模型,一个 API,一键部署',
openRouterPitch: '一个密钥,数百个模型 — 稳妥的默认选择',
apiKeyOptions: {
fireworks: { short: '最快推理', description: '通过 Fireworks AI 直接访问 300+ 开源与专有模型 — 一个 API 密钥,生产级速度。' },
openrouter: { short: '一个密钥,多个模型', description: '用一个密钥访问数百个模型。适合新安装的默认选择。' },
openai: { short: 'GPT 级模型', description: '直接访问 OpenAI 模型。' },
gemini: { short: 'Gemini 模型', description: '直接访问 Google Gemini 模型。' },
Expand Down
16 changes: 16 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3520,6 +3520,22 @@ def _ensure_hermes_home_managed(home: Path):
"category": "provider",
"advanced": True,
},
"FIREWORKS_API_KEY": {
"description": "Fireworks AI API key",
"prompt": "Fireworks AI API key",
"url": "https://app.fireworks.ai/settings/users/api-keys",
"password": True,
"category": "provider",
"advanced": True,
},
"FIREWORKS_BASE_URL": {
"description": "Fireworks AI base URL override",
"prompt": "Fireworks AI base URL (leave empty for default)",
"url": None,
"password": False,
"category": "provider",
"advanced": True,
},
"MINIMAX_API_KEY": {
"description": "MiniMax API key (international)",
"prompt": "MiniMax API key",
Expand Down
5 changes: 5 additions & 0 deletions hermes_cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"KIMI_API_KEY",
"KIMI_CN_API_KEY",
"GMI_API_KEY",
"FIREWORKS_API_KEY",
"MINIMAX_API_KEY",
"MINIMAX_CN_API_KEY",
"KILOCODE_API_KEY",
Expand Down Expand Up @@ -845,6 +846,10 @@ def run_doctor(args):
"lmstudio",
"nous",
"nvidia",
# Fireworks' native model IDs are slash-form
# (accounts/fireworks/models/... and .../routers/...), so a "/"
# is expected, not an aggregator vendor prefix.
"fireworks",
}
provider_accepts_vendor_slug = (
provider_policy_id in providers_accepting_vendor_slugs
Expand Down
15 changes: 14 additions & 1 deletion hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,9 @@ class ProviderEntry(NamedTuple):

CANONICAL_PROVIDERS: list[ProviderEntry] = [
ProviderEntry("nous", "Nous Portal", "Nous Portal (Everything your agent needs, 300+ models with bundled tool use)"),
# Fireworks AI — preferred BYOK inference partner. Sits directly below Nous
# Portal (Nous's own provider stays first); everything else follows.
ProviderEntry("fireworks", "Fireworks AI", "Fireworks AI (Fastest inference for production AI, 300+ models, one API key)"),
ProviderEntry("openrouter", "OpenRouter", "OpenRouter (Pay-per-use API aggregator)"),
ProviderEntry("moa", "Mixture of Agents", "Mixture of Agents (named presets; aggregator acts after reference models)"),
ProviderEntry("novita", "NovitaAI", "NovitaAI (Cloud: Model API, Agent Sandbox, GPU Cloud)"),
Expand Down Expand Up @@ -1224,6 +1227,8 @@ def group_providers(slugs):
"arceeai": "arcee",
"gmi-cloud": "gmi",
"gmicloud": "gmi",
"fireworks-ai": "fireworks",
"fw": "fireworks",
"minimax-china": "minimax-cn",
"minimax_cn": "minimax-cn",
"minimax-portal": "minimax-oauth",
Expand Down Expand Up @@ -2472,7 +2477,15 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
# live API is the authoritative catalog, so they merge
# live-first — live entries lead and stale curated entries
# no longer pollute the top of the picker. (#49129)
curated = list(_PROVIDER_MODELS.get(normalized, []))
#
# Plugin providers with no static _PROVIDER_MODELS entry fall
# back to the profile's curated fallback_models so their
# agentic picks lead the picker instead of whatever the live
# catalog happens to return first (e.g. Fireworks lists an
# image model, flux-*, ahead of its chat models).
curated = list(_PROVIDER_MODELS.get(normalized, [])) or list(
_p.fallback_models or ()
)
if curated:
if normalized in _LIVE_FIRST_PICKER_PROVIDERS:
primary, secondary = live, curated
Expand Down
10 changes: 10 additions & 0 deletions hermes_cli/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ class HermesOverlay:
base_url_override="https://api.gmi-serving.com/v1",
base_url_env_var="GMI_BASE_URL",
),
"fireworks": HermesOverlay(
transport="openai_chat",
extra_env_vars=("FIREWORKS_API_KEY",),
base_url_override="https://api.fireworks.ai/inference/v1",
base_url_env_var="FIREWORKS_BASE_URL",
),
"ollama-cloud": HermesOverlay(
transport="openai_chat",
base_url_override="https://ollama.com/v1",
Expand Down Expand Up @@ -343,6 +349,10 @@ class ProviderDef:
"gmi-cloud": "gmi",
"gmicloud": "gmi",

# fireworks
"fireworks-ai": "fireworks",
"fw": "fireworks",

# Local server aliases → virtual "local" concept (resolved via user config)
"lmstudio": "lmstudio",
"lm-studio": "lmstudio",
Expand Down
Loading