feat(desktop): promote Fireworks AI to #2 in onboarding provider picker (#66432)

Mirror CANONICAL_PROVIDERS so Fireworks sits directly under Nous Portal
(always visible) ahead of OpenRouter across onboarding, Settings → Providers,
and the API-key catalog.
This commit is contained in:
brooklyn! 2026-07-17 14:19:57 -04:00 committed by GitHub
parent 594308d4bb
commit 2f00cca49a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 102 additions and 21 deletions

View file

@ -45,6 +45,15 @@ export const PROVIDER_GROUPS: ProviderPrefix[] = [
docsUrl: 'https://portal.nousresearch.com',
priority: 0
},
{
prefix: 'FIREWORKS_',
name: 'Fireworks AI',
description: 'OpenAI-compatible direct model API',
docsUrl: 'https://app.fireworks.ai/settings/users/api-keys',
// Slot #2 — mirrors CANONICAL_PROVIDERS (after Nous, ahead of OpenRouter).
// Same numeric priority as OpenRouter; name sort puts Fireworks first.
priority: 1
},
{
prefix: 'OPENROUTER_',
name: 'OpenRouter',

View file

@ -122,6 +122,7 @@ describe('settings helpers', () => {
it('maps a provider env var to its labeled group', () => {
expect(providerGroup('XAI_API_KEY')).toBe('xAI')
expect(providerGroup('NOUS_API_KEY')).toBe('Nous Portal')
expect(providerGroup('FIREWORKS_API_KEY')).toBe('Fireworks AI')
expect(providerGroup('OPENROUTER_API_KEY')).toBe('OpenRouter')
})

View file

@ -6,7 +6,8 @@ import { runInTerminal } from '@/app/right-sidebar/store'
import {
FEATURED_ID,
FeaturedProviderRow,
KeyProviderRow,
FireworksProviderRow,
OpenRouterProviderRow,
ProviderRow,
providerTitle,
sortProviders
@ -114,11 +115,12 @@ function buildProviderKeyGroups(vars: Record<string, EnvVarInfo>): ProviderKeyGr
// Deliberately a near-1:1 replica of the first-run onboarding picker
// (`Picker` in desktop-onboarding-overlay): same recommended card, same
// provider rows, same "Other providers" disclosure, same OpenRouter quick-key
// row, and the same bottom-right "I have an API key" affordance. The leaf cards
// are the exact shared components, so the two surfaces stay visually identical.
// Selecting a provider hands off to the shared onboarding overlay, which runs
// that provider's real sign-in flow; the key affordances open the API-key
// Fireworks #2 quick-key row, same provider rows, same "Other providers"
// disclosure, same OpenRouter quick-key row, and the same bottom-right
// "I have an API key" affordance. The leaf cards are the exact shared
// components, so the two surfaces stay visually identical. Selecting a
// provider hands off to the shared onboarding overlay, which runs that
// provider's real sign-in flow; the key affordances open the API-key
// catalog below.
function OAuthPicker({
disconnecting,
@ -172,6 +174,8 @@ function OAuthPicker({
{p.intro}
</p>
{featured && <FeaturedProviderRow onSelect={select} provider={featured} />}
{/* Slot #2 — always visible, matching onboarding / CANONICAL_PROVIDERS. */}
<FireworksProviderRow onClick={onWantApiKey} />
{connected.length > 0 && (
<>
<GroupLabel>{p.connected}</GroupLabel>
@ -193,7 +197,7 @@ function OAuthPicker({
{others.map(p => (
<ProviderRow key={p.id} onSelect={select} provider={p} />
))}
<KeyProviderRow onClick={onWantApiKey} />
<OpenRouterProviderRow onClick={onWantApiKey} />
</>
)}
{collapsible && (

View file

@ -62,6 +62,9 @@ describe('onboarding Picker', () => {
expect(screen.getByText('Nous Portal')).toBeTruthy()
expect(screen.getByText('Recommended')).toBeTruthy()
// Fireworks is the always-visible #2 slot (after Nous), even while OAuth
// alternatives stay collapsed behind the disclosure.
expect(screen.getByText('Fireworks AI')).toBeTruthy()
expect(screen.queryByText('Anthropic API Key')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Other providers' }))
@ -70,10 +73,32 @@ describe('onboarding Picker', () => {
expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy()
})
it('shows Fireworks in slot #2 ahead of other OAuth providers', () => {
setProviders([
provider('openai-codex', 'OpenAI Codex / ChatGPT'),
provider('minimax-oauth', 'MiniMax'),
provider('nous', 'Nous Portal')
])
render(<Picker ctx={ctx} />)
fireEvent.click(screen.getByRole('button', { name: 'Other providers' }))
const labels = screen
.getAllByRole('button')
.map(el => el.textContent ?? '')
.filter(text => /Nous Portal|Fireworks AI|OpenAI OAuth|MiniMax|OpenRouter/.test(text))
const indexOf = (needle: string) => labels.findIndex(text => text.includes(needle))
expect(indexOf('Nous Portal')).toBeGreaterThanOrEqual(0)
expect(indexOf('Fireworks AI')).toBeGreaterThan(indexOf('Nous Portal'))
expect(indexOf('OpenAI OAuth')).toBeGreaterThan(indexOf('Fireworks AI'))
expect(indexOf('MiniMax')).toBeGreaterThan(indexOf('OpenAI OAuth'))
})
it('shows every provider directly when Nous Portal is absent', () => {
setProviders([provider('anthropic', 'Anthropic Claude'), provider('openai-codex', 'OpenAI Codex / ChatGPT')])
render(<Picker ctx={ctx} />)
expect(screen.getByText('Fireworks AI')).toBeTruthy()
expect(screen.getByText('Anthropic API Key')).toBeTruthy()
expect(screen.getByText('OpenAI OAuth (ChatGPT)')).toBeTruthy()
expect(screen.queryByText('Other sign-in options')).toBeNull()

View file

@ -28,9 +28,23 @@ import {
import type { ModelOptionProvider, OAuthProvider } from '@/types/hermes'
import { DocsLink, FlowPanel, Status } from './flow'
import { FeaturedProviderRow, KeyProviderRow, ProviderRow, sortProviders } from './providers'
import {
FeaturedProviderRow,
FireworksProviderRow,
OpenRouterProviderRow,
ProviderRow,
sortProviders
} from './providers'
export { FeaturedProviderRow, KeyProviderRow, ProviderRow, providerTitle, sortProviders } from './providers'
export {
FeaturedProviderRow,
FireworksProviderRow,
KeyProviderRow,
OpenRouterProviderRow,
ProviderRow,
providerTitle,
sortProviders
} from './providers'
interface DesktopOnboardingOverlayProps {
enabled: boolean
@ -48,19 +62,21 @@ export interface ApiKeyOption {
short?: string
}
// Curated order mirrors CANONICAL_PROVIDERS: Fireworks sits #2 overall (after
// Nous Portal OAuth), ahead of OpenRouter and the rest of the key catalog.
const API_KEY_OPTIONS: ApiKeyOption[] = [
{
id: 'openrouter',
name: 'OpenRouter',
envKey: 'OPENROUTER_API_KEY',
docsUrl: 'https://openrouter.ai/keys'
},
{
id: 'fireworks',
name: 'Fireworks AI',
envKey: 'FIREWORKS_API_KEY',
docsUrl: 'https://app.fireworks.ai/settings/users/api-keys'
},
{
id: 'openrouter',
name: 'OpenRouter',
envKey: 'OPENROUTER_API_KEY',
docsUrl: 'https://openrouter.ai/keys'
},
{
id: 'openai',
name: 'OpenAI',
@ -453,12 +469,14 @@ 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}
{/* Slot #2 — always visible, matching CANONICAL_PROVIDERS (Nous → Fireworks). */}
<FireworksProviderRow onClick={() => openKeyForm('FIREWORKS_API_KEY')} />
{showRest ? (
<>
{rest.map(p => (
<ProviderRow key={p.id} onSelect={select} provider={p} />
))}
<KeyProviderRow onClick={() => openKeyForm('OPENROUTER_API_KEY')} />
<OpenRouterProviderRow onClick={() => openKeyForm('OPENROUTER_API_KEY')} />
</>
) : null}
</div>

View file

@ -76,20 +76,39 @@ function ConnectedTag() {
const PROVIDER_ROW_CLASS =
'group flex w-full items-center justify-between gap-3 rounded-[6px] px-3 py-2.5 text-left transition-colors hover:bg-(--ui-control-hover-background)'
export function KeyProviderRow({ onClick }: { onClick: () => void }) {
const { t } = useI18n()
/** Quick-key row for API-key providers (Fireworks #2 after Nous, OpenRouter further down). */
export function KeyProviderRow({
onClick,
pitch,
title
}: {
onClick: () => void
pitch: string
title: string
}) {
return (
<RowButton className={PROVIDER_ROW_CLASS} onClick={onClick}>
<div className="min-w-0">
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">OpenRouter</span>
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.openRouterPitch}</p>
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">{title}</span>
<p className="mt-1 text-xs leading-5 text-muted-foreground">{pitch}</p>
</div>
<ChevronRight className="size-4 text-muted-foreground transition group-hover:text-foreground" />
</RowButton>
)
}
export function FireworksProviderRow({ onClick }: { onClick: () => void }) {
const { t } = useI18n()
return <KeyProviderRow onClick={onClick} pitch={t.onboarding.fireworksPitch} title="Fireworks AI" />
}
export function OpenRouterProviderRow({ onClick }: { onClick: () => void }) {
const { t } = useI18n()
return <KeyProviderRow onClick={onClick} pitch={t.onboarding.openRouterPitch} title="OpenRouter" />
}
export function ProviderRow({
onSelect,
provider

View file

@ -1971,6 +1971,7 @@ export const en: Translations = {
recommended: 'Recommended',
connected: 'Connected',
featuredPitch: 'One subscription, 300+ frontier models — the recommended way to run Hermes',
fireworksPitch: 'Direct model API — Fireworks-hosted frontier models',
openRouterPitch: 'One key, hundreds of models — a solid default',
apiKeyOptions: {
fireworks: {

View file

@ -1890,6 +1890,7 @@ export const ja = defineLocale({
recommended: '推奨',
connected: '接続済み',
featuredPitch: '1 つのサブスクリプションで 300 以上の最先端モデル — Hermes を実行するための推奨方法',
fireworksPitch: '直接モデル API — Fireworks がホストする最先端モデル',
openRouterPitch: '1 つのキーで数百のモデル — 堅実なデフォルト',
apiKeyOptions: {
fireworks: {

View file

@ -1622,6 +1622,7 @@ export interface Translations {
recommended: string
connected: string
featuredPitch: string
fireworksPitch: string
openRouterPitch: string
apiKeyOptions: Record<string, { short: string; description: string }>
backToSignIn: string

View file

@ -1834,6 +1834,7 @@ export const zhHant = defineLocale({
recommended: '建議',
connected: '已連線',
featuredPitch: '一個訂閱300+ 前沿模型 — 執行 Hermes 的建議方式',
fireworksPitch: '直接模型 API — Fireworks 託管的前沿模型',
openRouterPitch: '一個金鑰,數百個模型 — 穩定的預設選擇',
apiKeyOptions: {
fireworks: { short: '直接模型 API', description: '直接存取 Fireworks AI 託管的模型。' },

View file

@ -2144,6 +2144,7 @@ export const zh: Translations = {
recommended: '推荐',
connected: '已连接',
featuredPitch: '一个订阅300+ 前沿模型 — 运行 Hermes 的推荐方式',
fireworksPitch: '直接模型 API — Fireworks 托管的前沿模型',
openRouterPitch: '一个密钥,数百个模型 — 稳妥的默认选择',
apiKeyOptions: {
fireworks: { short: '直接模型 API', description: '直接访问 Fireworks AI 托管的模型。' },