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:
parent
594308d4bb
commit
2f00cca49a
11 changed files with 102 additions and 21 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -1890,6 +1890,7 @@ export const ja = defineLocale({
|
|||
recommended: '推奨',
|
||||
connected: '接続済み',
|
||||
featuredPitch: '1 つのサブスクリプションで 300 以上の最先端モデル — Hermes を実行するための推奨方法',
|
||||
fireworksPitch: '直接モデル API — Fireworks がホストする最先端モデル',
|
||||
openRouterPitch: '1 つのキーで数百のモデル — 堅実なデフォルト',
|
||||
apiKeyOptions: {
|
||||
fireworks: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1834,6 +1834,7 @@ export const zhHant = defineLocale({
|
|||
recommended: '建議',
|
||||
connected: '已連線',
|
||||
featuredPitch: '一個訂閱,300+ 前沿模型 — 執行 Hermes 的建議方式',
|
||||
fireworksPitch: '直接模型 API — Fireworks 託管的前沿模型',
|
||||
openRouterPitch: '一個金鑰,數百個模型 — 穩定的預設選擇',
|
||||
apiKeyOptions: {
|
||||
fireworks: { short: '直接模型 API', description: '直接存取 Fireworks AI 託管的模型。' },
|
||||
|
|
|
|||
|
|
@ -2144,6 +2144,7 @@ export const zh: Translations = {
|
|||
recommended: '推荐',
|
||||
connected: '已连接',
|
||||
featuredPitch: '一个订阅,300+ 前沿模型 — 运行 Hermes 的推荐方式',
|
||||
fireworksPitch: '直接模型 API — Fireworks 托管的前沿模型',
|
||||
openRouterPitch: '一个密钥,数百个模型 — 稳妥的默认选择',
|
||||
apiKeyOptions: {
|
||||
fireworks: { short: '直接模型 API', description: '直接访问 Fireworks AI 托管的模型。' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue