Expand inference provider catalog (#3057) (#3062)

This commit is contained in:
Steven Enamakel
2026-05-30 22:37:06 -07:00
committed by GitHub
parent f64f10e3c1
commit 4e221e978b
12 changed files with 743 additions and 168 deletions
+26 -103
View File
@@ -53,6 +53,13 @@ import {
import { ConfirmationModal } from '../../intelligence/ConfirmationModal';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
import {
authStyleForBuiltinCloudProvider,
BUILTIN_CLOUD_PROVIDER_META,
BUILTIN_CLOUD_PROVIDER_SLUGS,
builtinCloudProvider,
defaultEndpointForBuiltinCloudProvider,
} from './builtinCloudProviders';
import { presentProviderSetupError, ProviderSetupErrorNotice } from './ProviderSetupErrorNotice';
import { useReembedBackfillModal } from './useReembedBackfillModal';
@@ -105,6 +112,15 @@ const ROUTING_WORKLOAD_IDS: WorkloadId[] = [
'learning',
'subconscious',
];
const BUILTIN_RESERVED_SLUGS = [
'cloud',
'openhuman',
'pid',
'custom',
'ollama',
'lmstudio',
...BUILTIN_CLOUD_PROVIDER_SLUGS,
];
// ─────────────────────────────────────────────────────────────────────────────
// Static catalog
@@ -117,34 +133,7 @@ const BUILTIN_PROVIDER_META: Record<string, { tone: string; label: string }> = {
label: 'Managed',
tone: 'bg-emerald-50 dark:bg-emerald-500/10 ring-emerald-200 text-emerald-900 dark:text-emerald-100',
},
openai: {
label: 'OpenAI',
tone: 'bg-emerald-50 dark:bg-emerald-500/10 ring-emerald-200 text-emerald-900 dark:text-emerald-100',
},
anthropic: {
label: 'Anthropic',
tone: 'bg-orange-50 dark:bg-orange-500/10 ring-orange-200 text-orange-900 dark:text-orange-100',
},
openrouter: {
label: 'OpenRouter',
tone: 'bg-slate-100 dark:bg-slate-500/15 ring-slate-300 text-slate-900 dark:text-slate-100',
},
orcarouter: {
label: 'OrcaRouter',
tone: 'bg-sky-50 dark:bg-sky-500/10 ring-sky-200 text-sky-900 dark:text-sky-100',
},
gmi: {
label: 'GMI',
tone: 'bg-fuchsia-50 dark:bg-fuchsia-500/10 ring-fuchsia-200 text-fuchsia-900 dark:text-fuchsia-100',
},
fireworks: {
label: 'Fireworks',
tone: 'bg-rose-50 dark:bg-rose-500/10 ring-rose-200 text-rose-900 dark:text-rose-100',
},
moonshot: {
label: 'Kimi (Moonshot)',
tone: 'bg-indigo-50 dark:bg-indigo-500/10 ring-indigo-200 text-indigo-900 dark:text-indigo-100',
},
...BUILTIN_CLOUD_PROVIDER_META,
custom: {
label: 'Advanced',
tone: 'bg-sky-50 dark:bg-sky-500/10 ring-sky-200 text-sky-900 dark:text-sky-100',
@@ -259,9 +248,8 @@ function slugifyCustomProviderName(name: string): string {
*/
function authStyleForSlug(slug: string): AuthStyle {
if (slug === 'openhuman') return 'openhuman_jwt';
if (slug === 'anthropic') return 'anthropic';
if (slug === 'lmstudio' || slug === 'ollama') return 'none';
return 'bearer';
return authStyleForBuiltinCloudProvider(slug) ?? 'bearer';
}
function toPanelProvider(p: CloudProviderView): CloudProvider {
@@ -615,21 +603,7 @@ const ProviderKeyDialog = ({
const placeholder = isLocalRuntime
? defaultEndpointFor(slug) || t('settings.ai.defaultLocalEndpoint')
: slug === 'openai'
? 'sk-...'
: slug === 'anthropic'
? 'sk-ant-...'
: slug === 'openrouter'
? 'sk-or-...'
: slug === 'orcarouter'
? 'sk-orca-...'
: slug === 'gmi'
? 'gmi-...'
: slug === 'fireworks'
? 'fw-...'
: slug === 'moonshot'
? 'sk-...'
: 'your-api-key';
: (builtinCloudProvider(slug)?.keyPlaceholder ?? 'your-api-key');
const fieldLabel = isLocalRuntime
? t('settings.ai.endpointUrlLabel')
@@ -2808,18 +2782,8 @@ const AIPanel = ({ embedded = false }: AIPanelProps = {}) => {
onToggle={() => {}}
/>
{/* Built-in cloud providers — openai/anthropic/openrouter/orcarouter/custom */}
{(
[
'openai',
'anthropic',
'openrouter',
'orcarouter',
'gmi',
'fireworks',
'moonshot',
] as const
).map(slug => {
{/* Built-in cloud providers */}
{BUILTIN_CLOUD_PROVIDER_SLUGS.map(slug => {
const meta = BUILTIN_PROVIDER_META[slug];
const label = meta?.label ?? slug;
const existing = draft.cloudProviders.find(cp => cp.slug === slug);
@@ -2860,21 +2824,7 @@ const AIPanel = ({ embedded = false }: AIPanelProps = {}) => {
})}
{draft.cloudProviders
.filter(
cp =>
![
'openhuman',
'openai',
'anthropic',
'openrouter',
'orcarouter',
'gmi',
'fireworks',
'moonshot',
'lmstudio',
'ollama',
].includes(cp.slug)
)
.filter(cp => !BUILTIN_RESERVED_SLUGS.includes(cp.slug))
.map(existing => (
<ProviderToggleChip
key={existing.id}
@@ -3342,23 +3292,7 @@ const CloudProviderEditor = ({
const [saving, setSaving] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const slug = initial?.slug ?? slugifyCustomProviderName(label);
const hasReservedSlugCollision =
!initial &&
[
'cloud',
'openhuman',
'pid',
'openai',
'anthropic',
'openrouter',
'orcarouter',
'gmi',
'fireworks',
'moonshot',
'custom',
'ollama',
'lmstudio',
].includes(slug);
const hasReservedSlugCollision = !initial && BUILTIN_RESERVED_SLUGS.includes(slug);
const slugError = !slug
? t('settings.ai.slugMissingError')
: existingSlugs.includes(slug)
@@ -3504,23 +3438,12 @@ const CloudProviderEditor = ({
};
function defaultEndpointFor(slug: string): string {
const builtinEndpoint = defaultEndpointForBuiltinCloudProvider(slug);
if (builtinEndpoint) return builtinEndpoint;
switch (slug) {
case 'openhuman':
return 'https://api.openhuman.ai/v1';
case 'openai':
return 'https://api.openai.com/v1';
case 'anthropic':
return 'https://api.anthropic.com/v1';
case 'openrouter':
return 'https://openrouter.ai/api/v1';
case 'orcarouter':
return 'https://api.orcarouter.ai/v1';
case 'gmi':
return 'https://api.gmi-serving.com/v1';
case 'fireworks':
return 'https://api.fireworks.ai/inference/v1';
case 'moonshot':
return 'https://api.moonshot.ai/v1';
case 'ollama':
// Ollama exposes an OpenAI-compatible endpoint at /v1; the bare host is
// also accepted by the Rust factory (it appends /v1 internally for chat).
@@ -380,6 +380,82 @@ describe('AIPanel', () => {
).not.toBeInTheDocument();
});
it('renders Phase 1 built-in provider chips including SumoPod', async () => {
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
renderWithProviders(<AIPanel />);
for (const label of ['Groq', 'DeepSeek', 'MiniMax', 'SumoPod']) {
await waitFor(() =>
expect(
screen.getByRole('switch', { name: new RegExp(`Connect ${label}`, 'i') })
).toBeInTheDocument()
);
}
});
it('connects SumoPod with the native endpoint and provider:sumopod key', async () => {
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
renderWithProviders(<AIPanel />);
fireEvent.click(await screen.findByRole('switch', { name: /Connect SumoPod/i }));
const dialog = await screen.findByRole('dialog', { name: /Connect SumoPod/i });
fireEvent.change(within(dialog).getByLabelText(/API key/i), {
target: { value: 'sk-sumopod-test' },
});
fireEvent.click(within(dialog).getByRole('button', { name: /^Save$/i }));
await waitFor(() =>
expect(vi.mocked(setCloudProviderKey)).toHaveBeenCalledWith('sumopod', 'sk-sumopod-test')
);
await waitFor(() => expect(vi.mocked(listProviderModels)).toHaveBeenCalledWith('sumopod'));
await waitFor(() => expect(vi.mocked(saveAISettings)).toHaveBeenCalled());
const [, nextSettings] = vi.mocked(saveAISettings).mock.calls[0];
expect(nextSettings.cloudProviders).toEqual(
expect.arrayContaining([
expect.objectContaining({
slug: 'sumopod',
label: 'SumoPod',
endpoint: 'https://ai.sumopod.com/v1',
auth_style: 'bearer',
has_api_key: true,
}),
])
);
});
it('connects MiniMax with anthropic auth style', async () => {
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
renderWithProviders(<AIPanel />);
fireEvent.click(await screen.findByRole('switch', { name: /Connect MiniMax/i }));
const dialog = await screen.findByRole('dialog', { name: /Connect MiniMax/i });
fireEvent.change(within(dialog).getByLabelText(/API key/i), {
target: { value: 'sk-minimax-test' },
});
fireEvent.click(within(dialog).getByRole('button', { name: /^Save$/i }));
await waitFor(() =>
expect(vi.mocked(setCloudProviderKey)).toHaveBeenCalledWith('minimax', 'sk-minimax-test')
);
await waitFor(() => expect(vi.mocked(saveAISettings)).toHaveBeenCalled());
const [, nextSettings] = vi.mocked(saveAISettings).mock.calls[0];
expect(nextSettings.cloudProviders).toEqual(
expect.arrayContaining([
expect.objectContaining({
slug: 'minimax',
label: 'MiniMax',
endpoint: 'https://api.minimax.io/anthropic',
auth_style: 'anthropic',
}),
])
);
});
it('surfaces provider setup errors in an alert with technical details collapsed', async () => {
vi.mocked(loadAISettings).mockResolvedValue({ ...baseSettings, cloudProviders: [] });
vi.mocked(listProviderModels).mockRejectedValueOnce(
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';
import {
authStyleForBuiltinCloudProvider,
BUILTIN_CLOUD_PROVIDER_SLUGS,
BUILTIN_CLOUD_PROVIDERS,
defaultEndpointForBuiltinCloudProvider,
} from '../builtinCloudProviders';
describe('builtinCloudProviders', () => {
it('keeps built-in provider slugs unique', () => {
expect(new Set(BUILTIN_CLOUD_PROVIDER_SLUGS).size).toBe(BUILTIN_CLOUD_PROVIDER_SLUGS.length);
});
it.each([
['groq', 'https://api.groq.com/openai/v1', 'bearer'],
['deepseek', 'https://api.deepseek.com/v1', 'bearer'],
['minimax', 'https://api.minimax.io/anthropic', 'anthropic'],
['sumopod', 'https://ai.sumopod.com/v1', 'bearer'],
] as const)('maps %s to its endpoint and auth style', (slug, endpoint, authStyle) => {
expect(defaultEndpointForBuiltinCloudProvider(slug)).toBe(endpoint);
expect(authStyleForBuiltinCloudProvider(slug)).toBe(authStyle);
});
it('contains the full phase one provider set', () => {
expect(BUILTIN_CLOUD_PROVIDERS.map(provider => provider.slug)).toEqual(
expect.arrayContaining([
'groq',
'mistral',
'deepseek',
'together',
'google',
'cerebras',
'xai',
'huggingface',
'nvidia',
'zai',
'minimax',
'stepfun',
'kilocode',
'deepinfra',
'novita',
'venice',
'vercel-ai-gateway',
'sumopod',
])
);
});
});
@@ -0,0 +1,236 @@
import type { AuthStyle } from '../../../utils/tauriCommands/config';
export type BuiltinCloudProvider = {
slug: string;
label: string;
endpoint: string;
authStyle: AuthStyle;
tone: string;
keyPlaceholder?: string;
};
const TONE = {
emerald:
'bg-emerald-50 dark:bg-emerald-500/10 ring-emerald-200 text-emerald-900 dark:text-emerald-100',
orange: 'bg-orange-50 dark:bg-orange-500/10 ring-orange-200 text-orange-900 dark:text-orange-100',
slate: 'bg-slate-100 dark:bg-slate-500/15 ring-slate-300 text-slate-900 dark:text-slate-100',
sky: 'bg-sky-50 dark:bg-sky-500/10 ring-sky-200 text-sky-900 dark:text-sky-100',
fuchsia:
'bg-fuchsia-50 dark:bg-fuchsia-500/10 ring-fuchsia-200 text-fuchsia-900 dark:text-fuchsia-100',
rose: 'bg-rose-50 dark:bg-rose-500/10 ring-rose-200 text-rose-900 dark:text-rose-100',
indigo: 'bg-indigo-50 dark:bg-indigo-500/10 ring-indigo-200 text-indigo-900 dark:text-indigo-100',
amber: 'bg-amber-50 dark:bg-amber-500/10 ring-amber-200 text-amber-900 dark:text-amber-100',
teal: 'bg-teal-50 dark:bg-teal-500/10 ring-teal-200 text-teal-900 dark:text-teal-100',
violet: 'bg-violet-50 dark:bg-violet-500/10 ring-violet-200 text-violet-900 dark:text-violet-100',
zinc: 'bg-zinc-100 dark:bg-zinc-500/15 ring-zinc-300 text-zinc-900 dark:text-zinc-100',
} as const;
export const BUILTIN_CLOUD_PROVIDERS: BuiltinCloudProvider[] = [
{
slug: 'openai',
label: 'OpenAI',
endpoint: 'https://api.openai.com/v1',
authStyle: 'bearer',
tone: TONE.emerald,
keyPlaceholder: 'sk-...',
},
{
slug: 'anthropic',
label: 'Anthropic',
endpoint: 'https://api.anthropic.com/v1',
authStyle: 'anthropic',
tone: TONE.orange,
keyPlaceholder: 'sk-ant-...',
},
{
slug: 'openrouter',
label: 'OpenRouter',
endpoint: 'https://openrouter.ai/api/v1',
authStyle: 'bearer',
tone: TONE.slate,
keyPlaceholder: 'sk-or-...',
},
{
slug: 'orcarouter',
label: 'OrcaRouter',
endpoint: 'https://api.orcarouter.ai/v1',
authStyle: 'bearer',
tone: TONE.sky,
keyPlaceholder: 'sk-orca-...',
},
{
slug: 'gmi',
label: 'GMI',
endpoint: 'https://api.gmi-serving.com/v1',
authStyle: 'bearer',
tone: TONE.fuchsia,
keyPlaceholder: 'gmi-...',
},
{
slug: 'fireworks',
label: 'Fireworks',
endpoint: 'https://api.fireworks.ai/inference/v1',
authStyle: 'bearer',
tone: TONE.rose,
keyPlaceholder: 'fw-...',
},
{
slug: 'moonshot',
label: 'Kimi (Moonshot)',
endpoint: 'https://api.moonshot.ai/v1',
authStyle: 'bearer',
tone: TONE.indigo,
keyPlaceholder: 'sk-...',
},
{
slug: 'groq',
label: 'Groq',
endpoint: 'https://api.groq.com/openai/v1',
authStyle: 'bearer',
tone: TONE.teal,
keyPlaceholder: 'gsk_...',
},
{
slug: 'mistral',
label: 'Mistral',
endpoint: 'https://api.mistral.ai/v1',
authStyle: 'bearer',
tone: TONE.amber,
},
{
slug: 'deepseek',
label: 'DeepSeek',
endpoint: 'https://api.deepseek.com/v1',
authStyle: 'bearer',
tone: TONE.zinc,
keyPlaceholder: 'sk-...',
},
{
slug: 'together',
label: 'Together AI',
endpoint: 'https://api.together.xyz/v1',
authStyle: 'bearer',
tone: TONE.violet,
},
{
slug: 'google',
label: 'Google Gemini',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/openai',
authStyle: 'bearer',
tone: TONE.sky,
},
{
slug: 'cerebras',
label: 'Cerebras',
endpoint: 'https://api.cerebras.ai/v1',
authStyle: 'bearer',
tone: TONE.orange,
},
{
slug: 'xai',
label: 'xAI',
endpoint: 'https://api.x.ai/v1',
authStyle: 'bearer',
tone: TONE.zinc,
},
{
slug: 'huggingface',
label: 'Hugging Face',
endpoint: 'https://router.huggingface.co/v1',
authStyle: 'bearer',
tone: TONE.amber,
keyPlaceholder: 'hf_...',
},
{
slug: 'nvidia',
label: 'NVIDIA',
endpoint: 'https://integrate.api.nvidia.com/v1',
authStyle: 'bearer',
tone: TONE.emerald,
},
{
slug: 'zai',
label: 'Z.AI',
endpoint: 'https://api.z.ai/api/paas/v4',
authStyle: 'bearer',
tone: TONE.teal,
},
{
slug: 'minimax',
label: 'MiniMax',
endpoint: 'https://api.minimax.io/anthropic',
authStyle: 'anthropic',
tone: TONE.rose,
},
{
slug: 'stepfun',
label: 'StepFun',
endpoint: 'https://api.stepfun.ai/step_plan/v1',
authStyle: 'bearer',
tone: TONE.indigo,
},
{
slug: 'kilocode',
label: 'Kilo Code',
endpoint: 'https://api.kilo.ai/api/gateway',
authStyle: 'bearer',
tone: TONE.fuchsia,
},
{
slug: 'deepinfra',
label: 'DeepInfra',
endpoint: 'https://api.deepinfra.com/v1/openai',
authStyle: 'bearer',
tone: TONE.slate,
},
{
slug: 'novita',
label: 'Novita',
endpoint: 'https://api.novita.ai/v3/openai',
authStyle: 'bearer',
tone: TONE.violet,
},
{
slug: 'venice',
label: 'Venice',
endpoint: 'https://api.venice.ai/api/v1',
authStyle: 'bearer',
tone: TONE.teal,
},
{
slug: 'vercel-ai-gateway',
label: 'Vercel AI Gateway',
endpoint: 'https://ai-gateway.vercel.sh/v1',
authStyle: 'bearer',
tone: TONE.zinc,
},
{
slug: 'sumopod',
label: 'SumoPod',
endpoint: 'https://ai.sumopod.com/v1',
authStyle: 'bearer',
tone: TONE.amber,
keyPlaceholder: 'sk-...',
},
];
export const BUILTIN_CLOUD_PROVIDER_SLUGS = BUILTIN_CLOUD_PROVIDERS.map(provider => provider.slug);
export const BUILTIN_CLOUD_PROVIDER_META = Object.fromEntries(
BUILTIN_CLOUD_PROVIDERS.map(provider => [
provider.slug,
{ label: provider.label, tone: provider.tone },
])
) as Record<string, { tone: string; label: string }>;
export function builtinCloudProvider(slug: string): BuiltinCloudProvider | undefined {
return BUILTIN_CLOUD_PROVIDERS.find(provider => provider.slug === slug);
}
export function defaultEndpointForBuiltinCloudProvider(slug: string): string {
return builtinCloudProvider(slug)?.endpoint ?? '';
}
export function authStyleForBuiltinCloudProvider(slug: string): AuthStyle | undefined {
return builtinCloudProvider(slug)?.authStyle;
}