mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# Inference Provider Catalog
|
||||
|
||||
This document tracks the built-in BYOK inference-provider presets exposed in
|
||||
Settings > AI. OpenHuman still supports arbitrary OpenAI-compatible endpoints
|
||||
through a custom provider entry; this table is the first-class chip catalog.
|
||||
|
||||
## Phase 1 Presets
|
||||
|
||||
| Slug | Label | Endpoint | Auth style | Status |
|
||||
| ------------------- | ----------------- | --------------------------------------------------------- | ---------- | ------- |
|
||||
| `openai` | OpenAI | `https://api.openai.com/v1` | bearer | Shipped |
|
||||
| `anthropic` | Anthropic | `https://api.anthropic.com/v1` | anthropic | Shipped |
|
||||
| `openrouter` | OpenRouter | `https://openrouter.ai/api/v1` | bearer | Shipped |
|
||||
| `orcarouter` | OrcaRouter | `https://api.orcarouter.ai/v1` | bearer | Shipped |
|
||||
| `gmi` | GMI | `https://api.gmi-serving.com/v1` | bearer | Shipped |
|
||||
| `fireworks` | Fireworks | `https://api.fireworks.ai/inference/v1` | bearer | Shipped |
|
||||
| `moonshot` | Kimi (Moonshot) | `https://api.moonshot.ai/v1` | bearer | Shipped |
|
||||
| `groq` | Groq | `https://api.groq.com/openai/v1` | bearer | Shipped |
|
||||
| `mistral` | Mistral | `https://api.mistral.ai/v1` | bearer | Shipped |
|
||||
| `deepseek` | DeepSeek | `https://api.deepseek.com/v1` | bearer | Shipped |
|
||||
| `together` | Together AI | `https://api.together.xyz/v1` | bearer | Shipped |
|
||||
| `google` | Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | bearer | Shipped |
|
||||
| `cerebras` | Cerebras | `https://api.cerebras.ai/v1` | bearer | Shipped |
|
||||
| `xai` | xAI | `https://api.x.ai/v1` | bearer | Shipped |
|
||||
| `huggingface` | Hugging Face | `https://router.huggingface.co/v1` | bearer | Shipped |
|
||||
| `nvidia` | NVIDIA | `https://integrate.api.nvidia.com/v1` | bearer | Shipped |
|
||||
| `zai` | Z.AI | `https://api.z.ai/api/paas/v4` | bearer | Shipped |
|
||||
| `minimax` | MiniMax | `https://api.minimax.io/anthropic` | anthropic | Shipped |
|
||||
| `stepfun` | StepFun | `https://api.stepfun.ai/step_plan/v1` | bearer | Shipped |
|
||||
| `kilocode` | Kilo Code | `https://api.kilo.ai/api/gateway` | bearer | Shipped |
|
||||
| `deepinfra` | DeepInfra | `https://api.deepinfra.com/v1/openai` | bearer | Shipped |
|
||||
| `novita` | Novita | `https://api.novita.ai/v3/openai` | bearer | Shipped |
|
||||
| `venice` | Venice | `https://api.venice.ai/api/v1` | bearer | Shipped |
|
||||
| `vercel-ai-gateway` | Vercel AI Gateway | `https://ai-gateway.vercel.sh/v1` | bearer | Shipped |
|
||||
| `sumopod` | SumoPod | `https://ai.sumopod.com/v1` | bearer | Shipped |
|
||||
|
||||
API keys are stored through the auth-profile store under `provider:<slug>`;
|
||||
they are not read from environment variables by the desktop Settings flow.
|
||||
|
||||
## Deferred Transports
|
||||
|
||||
The following provider families need transport or credential work beyond a
|
||||
static OpenAI-compatible or Anthropic-compatible preset:
|
||||
|
||||
| Provider / family | Needed work |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| OpenAI Codex / ChatGPT subscription | Continue extending existing `openai_oauth` support. |
|
||||
| xAI OAuth, MiniMax OAuth, Qwen OAuth, Nous device-code | Add provider-specific OAuth or import flows. |
|
||||
| Google Gemini CLI, Claude CLI, Copilot ACP | Add subprocess or external-process transports. |
|
||||
| AWS Bedrock Converse | Add native AWS SDK / Bedrock Converse transport. |
|
||||
| Azure Foundry | Model dual OpenAI / Anthropic API modes and Azure credential handling. |
|
||||
| GitHub Copilot | Add GitHub-token import and provider transport. |
|
||||
@@ -1087,8 +1087,8 @@ pub(super) const CAPABILITIES: &[Capability] = &[
|
||||
name: "Configure AI",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Adjust AI-related settings and agent behavior preferences.",
|
||||
how_to: "Settings > Developer Options > AI Configuration",
|
||||
description: "Configure managed, local, custom, and built-in BYOK LLM providers, including SumoPod and other OpenAI-compatible gateways, plus per-workload routing preferences.",
|
||||
how_to: "Settings > AI",
|
||||
status: CapabilityStatus::Stable,
|
||||
privacy: None,
|
||||
},
|
||||
|
||||
@@ -12,6 +12,179 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BuiltinCloudProvider {
|
||||
pub slug: &'static str,
|
||||
pub label: &'static str,
|
||||
pub endpoint: &'static str,
|
||||
pub auth_style: AuthStyle,
|
||||
}
|
||||
|
||||
pub const BUILTIN_CLOUD_PROVIDERS: &[BuiltinCloudProvider] = &[
|
||||
BuiltinCloudProvider {
|
||||
slug: "openhuman",
|
||||
label: "OpenHuman",
|
||||
endpoint: "https://api.openhuman.ai/v1",
|
||||
auth_style: AuthStyle::OpenhumanJwt,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "openai",
|
||||
label: "OpenAI",
|
||||
endpoint: "https://api.openai.com/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "anthropic",
|
||||
label: "Anthropic",
|
||||
endpoint: "https://api.anthropic.com/v1",
|
||||
auth_style: AuthStyle::Anthropic,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "openrouter",
|
||||
label: "OpenRouter",
|
||||
endpoint: "https://openrouter.ai/api/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "orcarouter",
|
||||
label: "OrcaRouter",
|
||||
endpoint: "https://api.orcarouter.ai/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "gmi",
|
||||
label: "GMI",
|
||||
endpoint: "https://api.gmi-serving.com/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "fireworks",
|
||||
label: "Fireworks",
|
||||
endpoint: "https://api.fireworks.ai/inference/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "moonshot",
|
||||
label: "Kimi (Moonshot)",
|
||||
endpoint: "https://api.moonshot.ai/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "groq",
|
||||
label: "Groq",
|
||||
endpoint: "https://api.groq.com/openai/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "mistral",
|
||||
label: "Mistral",
|
||||
endpoint: "https://api.mistral.ai/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "deepseek",
|
||||
label: "DeepSeek",
|
||||
endpoint: "https://api.deepseek.com/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "together",
|
||||
label: "Together AI",
|
||||
endpoint: "https://api.together.xyz/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "google",
|
||||
label: "Google Gemini",
|
||||
endpoint: "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "cerebras",
|
||||
label: "Cerebras",
|
||||
endpoint: "https://api.cerebras.ai/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "xai",
|
||||
label: "xAI",
|
||||
endpoint: "https://api.x.ai/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "huggingface",
|
||||
label: "Hugging Face",
|
||||
endpoint: "https://router.huggingface.co/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "nvidia",
|
||||
label: "NVIDIA",
|
||||
endpoint: "https://integrate.api.nvidia.com/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "zai",
|
||||
label: "Z.AI",
|
||||
endpoint: "https://api.z.ai/api/paas/v4",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "minimax",
|
||||
label: "MiniMax",
|
||||
endpoint: "https://api.minimax.io/anthropic",
|
||||
auth_style: AuthStyle::Anthropic,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "stepfun",
|
||||
label: "StepFun",
|
||||
endpoint: "https://api.stepfun.ai/step_plan/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "kilocode",
|
||||
label: "Kilo Code",
|
||||
endpoint: "https://api.kilo.ai/api/gateway",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "deepinfra",
|
||||
label: "DeepInfra",
|
||||
endpoint: "https://api.deepinfra.com/v1/openai",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "novita",
|
||||
label: "Novita",
|
||||
endpoint: "https://api.novita.ai/v3/openai",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "venice",
|
||||
label: "Venice",
|
||||
endpoint: "https://api.venice.ai/api/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "vercel-ai-gateway",
|
||||
label: "Vercel AI Gateway",
|
||||
endpoint: "https://ai-gateway.vercel.sh/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
BuiltinCloudProvider {
|
||||
slug: "sumopod",
|
||||
label: "SumoPod",
|
||||
endpoint: "https://ai.sumopod.com/v1",
|
||||
auth_style: AuthStyle::Bearer,
|
||||
},
|
||||
];
|
||||
|
||||
fn builtin_cloud_provider(type_str: &str) -> Option<&'static BuiltinCloudProvider> {
|
||||
BUILTIN_CLOUD_PROVIDERS
|
||||
.iter()
|
||||
.find(|provider| provider.slug == type_str)
|
||||
}
|
||||
|
||||
/// Authentication header style for a cloud provider.
|
||||
///
|
||||
/// Wire format is lowercase (e.g. `"bearer"`). Determines which HTTP headers
|
||||
@@ -160,41 +333,24 @@ pub fn migrate_legacy_fields(entry: &mut CloudProviderCreds) {
|
||||
|
||||
// Auth style from legacy type when still at default Bearer.
|
||||
if entry.auth_style == AuthStyle::Bearer {
|
||||
match lt {
|
||||
"anthropic" => {
|
||||
entry.auth_style = AuthStyle::Anthropic;
|
||||
}
|
||||
"openhuman" => {
|
||||
entry.auth_style = AuthStyle::OpenhumanJwt;
|
||||
}
|
||||
_ => {}
|
||||
if let Some(provider) = builtin_cloud_provider(lt) {
|
||||
entry.auth_style = provider.auth_style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a legacy type string (or slug) to a human-readable label.
|
||||
fn legacy_label_for(type_str: &str) -> &'static str {
|
||||
match type_str {
|
||||
"openhuman" => "OpenHuman",
|
||||
"openai" => "OpenAI",
|
||||
"anthropic" => "Anthropic",
|
||||
"openrouter" => "OpenRouter",
|
||||
"orcarouter" => "OrcaRouter",
|
||||
"custom" => "Custom",
|
||||
_ => "Custom",
|
||||
}
|
||||
builtin_cloud_provider(type_str)
|
||||
.map(|provider| provider.label)
|
||||
.unwrap_or("Custom")
|
||||
}
|
||||
|
||||
/// Map a legacy type string to its well-known default endpoint.
|
||||
fn legacy_default_endpoint(type_str: &str) -> &'static str {
|
||||
match type_str {
|
||||
"openhuman" => "https://api.openhuman.ai/v1",
|
||||
"openai" => "https://api.openai.com/v1",
|
||||
"anthropic" => "https://api.anthropic.com/v1",
|
||||
"openrouter" => "https://openrouter.ai/api/v1",
|
||||
"orcarouter" => "https://api.orcarouter.ai/v1",
|
||||
_ => "",
|
||||
}
|
||||
builtin_cloud_provider(type_str)
|
||||
.map(|provider| provider.endpoint)
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
/// Generate a short opaque id for a new provider entry.
|
||||
@@ -303,7 +459,10 @@ impl CloudProviderType {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_slug_reserved;
|
||||
use super::{
|
||||
is_slug_reserved, migrate_legacy_fields, AuthStyle, CloudProviderCreds,
|
||||
BUILTIN_CLOUD_PROVIDERS,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn reserved_slugs() {
|
||||
@@ -329,4 +488,58 @@ mod tests {
|
||||
"lmstudio is a free-form OpenAI-compatible slug"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_cloud_provider_defaults_cover_phase_one_presets() {
|
||||
for (slug, label, endpoint, auth_style) in [
|
||||
(
|
||||
"groq",
|
||||
"Groq",
|
||||
"https://api.groq.com/openai/v1",
|
||||
AuthStyle::Bearer,
|
||||
),
|
||||
(
|
||||
"deepseek",
|
||||
"DeepSeek",
|
||||
"https://api.deepseek.com/v1",
|
||||
AuthStyle::Bearer,
|
||||
),
|
||||
(
|
||||
"minimax",
|
||||
"MiniMax",
|
||||
"https://api.minimax.io/anthropic",
|
||||
AuthStyle::Anthropic,
|
||||
),
|
||||
(
|
||||
"sumopod",
|
||||
"SumoPod",
|
||||
"https://ai.sumopod.com/v1",
|
||||
AuthStyle::Bearer,
|
||||
),
|
||||
] {
|
||||
let mut entry = CloudProviderCreds {
|
||||
id: format!("p_{slug}"),
|
||||
legacy_type: Some(slug.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
migrate_legacy_fields(&mut entry);
|
||||
|
||||
assert_eq!(entry.slug, slug);
|
||||
assert_eq!(entry.label, label);
|
||||
assert_eq!(entry.endpoint, endpoint);
|
||||
assert_eq!(entry.auth_style, auth_style);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_cloud_provider_slugs_are_unique() {
|
||||
let mut slugs = std::collections::HashSet::new();
|
||||
for provider in BUILTIN_CLOUD_PROVIDERS {
|
||||
assert!(
|
||||
slugs.insert(provider.slug),
|
||||
"duplicate built-in cloud provider slug {}",
|
||||
provider.slug
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,46 +12,49 @@ Unified inference domain: the canonical home for everything LLM/STT/TTS/embeddin
|
||||
- Run ChatGPT/Codex OAuth (PKCE) for the `openai` cloud slug and persist tokens in the encrypted auth-profile store.
|
||||
- Expose an OpenAI-compatible `/v1/*` HTTP endpoint guarded by a stable user-managed external bearer.
|
||||
- Detect device hardware profile and recommend/apply local model presets/tiers.
|
||||
- Maintain the built-in BYOK provider preset catalog used by Settings > AI.
|
||||
The current matrix lives in `docs/inference-provider-catalog.md`; credentials
|
||||
are stored under `provider:<slug>` in the auth-profile store.
|
||||
|
||||
## Key files
|
||||
|
||||
| File / dir | Role |
|
||||
| --- | --- |
|
||||
| `mod.rs` | Domain root; module decls + re-exports; wires `inference.*` controller schemas/controllers. |
|
||||
| `ops.rs` | Canonical handler file — `inference_*` business logic returning `RpcOutcome<T>`; delegates to `local`, `provider`, `sentiment`, `device`, `presets`, `openai_oauth`. Includes Sentry-noise suppression for expected provider/user-config failures. |
|
||||
| `schemas.rs` | `inference.*` controller schemas + `handle_*` fns + param DTOs. |
|
||||
| `types.rs` | Serde DTOs: `LocalAiStatus`, `LocalAiAssetsStatus`, `LocalAiDownloadsProgress`, `LocalAiEmbeddingResult`, `LocalAiSpeechResult`, `LocalAiTtsResult`, etc. |
|
||||
| `device.rs` | `DeviceProfile` hardware detection (RAM/CPU/GPU/OS), cached. |
|
||||
| `model_ids.rs` | Effective chat/vision/embedding/STT/TTS/quantization model id resolution from config. |
|
||||
| `model_context.rs` | Known model context-window sizes (`context_window_for_model`) for pre-dispatch budgeting. |
|
||||
| `presets.rs` | `ModelPreset`, `ModelTier`, `VisionMode`; tier recommendation + apply-to-config; MVP preset gating. |
|
||||
| `sentiment.rs` | `SentimentResult` + emotion/valence analysis via the local model. |
|
||||
| `parse.rs` / `paths.rs` | Output parsing helpers / on-disk model artifact paths. |
|
||||
| `local/` | Local runtime manager (was `local_ai/`). |
|
||||
| `local/core.rs` | `LocalAiService` singleton (`global`/`try_global`), `model_artifact_path`. |
|
||||
| `local/ops.rs` | Local RPC entrypoints (`local_ai_status/prompt/summarize/vision_prompt/embed/should_react`, `ReactionDecision`); re-exported as `local::rpc`. |
|
||||
| `local/schemas.rs` | `local_ai.*` controller schemas + handlers. |
|
||||
| `local/ollama.rs`, `local/lm_studio.rs` | Provider-specific runtime drivers; base-url resolution. |
|
||||
| `local/install*.rs`, `local/voice_install_common.rs` | Whisper/Piper install + shared download logic. |
|
||||
| `local/model_requirements.rs` | `MIN_CONTEXT_TOKENS`, `evaluate_context`, `ContextEligibility`. |
|
||||
| `local/service/` | `LocalAiService` impl split: `bootstrap`, `ollama_admin`, `public_infer`, `speech`, `vision_embed`, `whisper_engine`, `assets`, `spawn_marker`. |
|
||||
| `provider/` | Unified provider abstraction (was `providers/`). |
|
||||
| `provider/traits.rs` | `Provider` trait + `ChatMessage`/`ChatRequest`/`ChatResponse`/`ToolCall`/`UsageInfo`/`ProviderDelta` etc. |
|
||||
| `provider/factory.rs` | `create_chat_provider`, `provider_for_role`, provider-string grammar, local/cloud construction; `BYOK_INCOMPLETE_SENTINEL`. |
|
||||
| `provider/router.rs` | `RouterProvider` hint-based multi-model routing. |
|
||||
| `provider/reliable.rs` | Retry/backoff wrapper. |
|
||||
| `provider/compatible*.rs` | OpenAI-compatible provider (request dump/parse/stream/types). |
|
||||
| `provider/openhuman_backend.rs` | Managed OpenHuman backend provider (session JWT). |
|
||||
| `provider/claude_agent_sdk/` | Claude Agent SDK subprocess provider (`protocol.rs`, `subprocess.rs`). |
|
||||
| `provider/config_rejection.rs`, `provider/billing_error.rs` | Error classifiers (unknown-model / config rejection / budget exhausted). |
|
||||
| `provider/temperature.rs`, `provider/thread_context.rs` | Per-workload temperature override; thread context plumbing. |
|
||||
| `provider/ops.rs` | `list_configured_models`, SessionExpired publishing on auth failure. |
|
||||
| `provider/schemas.rs` | Provider-layer schemas. |
|
||||
| `voice/` | Inference implementations imported by `crate::openhuman::voice`. |
|
||||
| `voice/cloud_transcribe.rs`, `voice/local_transcribe.rs`, `voice/local_speech.rs` | STT (cloud + local) and local TTS. |
|
||||
| `voice/streaming.rs`, `voice/postprocess.rs`, `voice/hallucination.rs` | Streaming transcription, post-processing, hallucination filtering. |
|
||||
| `openai_oauth/` | ChatGPT/Codex OAuth: `config.rs` (Codex OAuth config), `flow.rs` (start/complete/status/disconnect), `store.rs` (token persistence). |
|
||||
| `http/` | OpenAI-compatible endpoint: `server.rs` (`router()`), `types.rs`; `EXTERNAL_OPENAI_COMPAT_PROVIDER` bearer id. |
|
||||
| File / dir | Role |
|
||||
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mod.rs` | Domain root; module decls + re-exports; wires `inference.*` controller schemas/controllers. |
|
||||
| `ops.rs` | Canonical handler file — `inference_*` business logic returning `RpcOutcome<T>`; delegates to `local`, `provider`, `sentiment`, `device`, `presets`, `openai_oauth`. Includes Sentry-noise suppression for expected provider/user-config failures. |
|
||||
| `schemas.rs` | `inference.*` controller schemas + `handle_*` fns + param DTOs. |
|
||||
| `types.rs` | Serde DTOs: `LocalAiStatus`, `LocalAiAssetsStatus`, `LocalAiDownloadsProgress`, `LocalAiEmbeddingResult`, `LocalAiSpeechResult`, `LocalAiTtsResult`, etc. |
|
||||
| `device.rs` | `DeviceProfile` hardware detection (RAM/CPU/GPU/OS), cached. |
|
||||
| `model_ids.rs` | Effective chat/vision/embedding/STT/TTS/quantization model id resolution from config. |
|
||||
| `model_context.rs` | Known model context-window sizes (`context_window_for_model`) for pre-dispatch budgeting. |
|
||||
| `presets.rs` | `ModelPreset`, `ModelTier`, `VisionMode`; tier recommendation + apply-to-config; MVP preset gating. |
|
||||
| `sentiment.rs` | `SentimentResult` + emotion/valence analysis via the local model. |
|
||||
| `parse.rs` / `paths.rs` | Output parsing helpers / on-disk model artifact paths. |
|
||||
| `local/` | Local runtime manager (was `local_ai/`). |
|
||||
| `local/core.rs` | `LocalAiService` singleton (`global`/`try_global`), `model_artifact_path`. |
|
||||
| `local/ops.rs` | Local RPC entrypoints (`local_ai_status/prompt/summarize/vision_prompt/embed/should_react`, `ReactionDecision`); re-exported as `local::rpc`. |
|
||||
| `local/schemas.rs` | `local_ai.*` controller schemas + handlers. |
|
||||
| `local/ollama.rs`, `local/lm_studio.rs` | Provider-specific runtime drivers; base-url resolution. |
|
||||
| `local/install*.rs`, `local/voice_install_common.rs` | Whisper/Piper install + shared download logic. |
|
||||
| `local/model_requirements.rs` | `MIN_CONTEXT_TOKENS`, `evaluate_context`, `ContextEligibility`. |
|
||||
| `local/service/` | `LocalAiService` impl split: `bootstrap`, `ollama_admin`, `public_infer`, `speech`, `vision_embed`, `whisper_engine`, `assets`, `spawn_marker`. |
|
||||
| `provider/` | Unified provider abstraction (was `providers/`). |
|
||||
| `provider/traits.rs` | `Provider` trait + `ChatMessage`/`ChatRequest`/`ChatResponse`/`ToolCall`/`UsageInfo`/`ProviderDelta` etc. |
|
||||
| `provider/factory.rs` | `create_chat_provider`, `provider_for_role`, provider-string grammar, local/cloud construction; `BYOK_INCOMPLETE_SENTINEL`. |
|
||||
| `provider/router.rs` | `RouterProvider` hint-based multi-model routing. |
|
||||
| `provider/reliable.rs` | Retry/backoff wrapper. |
|
||||
| `provider/compatible*.rs` | OpenAI-compatible provider (request dump/parse/stream/types). |
|
||||
| `provider/openhuman_backend.rs` | Managed OpenHuman backend provider (session JWT). |
|
||||
| `provider/claude_agent_sdk/` | Claude Agent SDK subprocess provider (`protocol.rs`, `subprocess.rs`). |
|
||||
| `provider/config_rejection.rs`, `provider/billing_error.rs` | Error classifiers (unknown-model / config rejection / budget exhausted). |
|
||||
| `provider/temperature.rs`, `provider/thread_context.rs` | Per-workload temperature override; thread context plumbing. |
|
||||
| `provider/ops.rs` | `list_configured_models`, SessionExpired publishing on auth failure. |
|
||||
| `provider/schemas.rs` | Provider-layer schemas. |
|
||||
| `voice/` | Inference implementations imported by `crate::openhuman::voice`. |
|
||||
| `voice/cloud_transcribe.rs`, `voice/local_transcribe.rs`, `voice/local_speech.rs` | STT (cloud + local) and local TTS. |
|
||||
| `voice/streaming.rs`, `voice/postprocess.rs`, `voice/hallucination.rs` | Streaming transcription, post-processing, hallucination filtering. |
|
||||
| `openai_oauth/` | ChatGPT/Codex OAuth: `config.rs` (Codex OAuth config), `flow.rs` (start/complete/status/disconnect), `store.rs` (token persistence). |
|
||||
| `http/` | OpenAI-compatible endpoint: `server.rs` (`router()`), `types.rs`; `EXTERNAL_OPENAI_COMPAT_PROVIDER` bearer id. |
|
||||
|
||||
## Public surface
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ fn subagent_lifecycle_records_and_clears_active() {
|
||||
dedicated_thread: false,
|
||||
prompt_chars: 42,
|
||||
worker_thread_id: None,
|
||||
display_name: None,
|
||||
});
|
||||
let s = m.snapshot();
|
||||
assert_eq!(s.active_subagent.as_deref(), Some("researcher"));
|
||||
|
||||
@@ -651,6 +651,26 @@ fn config_schema_helpers_cover_provider_voice_agent_and_channel_defaults() {
|
||||
migrate_legacy_fields(&mut custom_legacy);
|
||||
assert_eq!(custom_legacy.label, "Custom");
|
||||
assert!(custom_legacy.endpoint.is_empty());
|
||||
let mut sumopod_legacy = CloudProviderCreds {
|
||||
id: "provider-sumopod".to_string(),
|
||||
legacy_type: Some("sumopod".to_string()),
|
||||
..CloudProviderCreds::default()
|
||||
};
|
||||
migrate_legacy_fields(&mut sumopod_legacy);
|
||||
assert_eq!(sumopod_legacy.slug, "sumopod");
|
||||
assert_eq!(sumopod_legacy.label, "SumoPod");
|
||||
assert_eq!(sumopod_legacy.endpoint, "https://ai.sumopod.com/v1");
|
||||
assert_eq!(sumopod_legacy.auth_style, AuthStyle::Bearer);
|
||||
let mut minimax_legacy = CloudProviderCreds {
|
||||
id: "provider-minimax".to_string(),
|
||||
legacy_type: Some("minimax".to_string()),
|
||||
..CloudProviderCreds::default()
|
||||
};
|
||||
migrate_legacy_fields(&mut minimax_legacy);
|
||||
assert_eq!(minimax_legacy.slug, "minimax");
|
||||
assert_eq!(minimax_legacy.label, "MiniMax");
|
||||
assert_eq!(minimax_legacy.endpoint, "https://api.minimax.io/anthropic");
|
||||
assert_eq!(minimax_legacy.auth_style, AuthStyle::Anthropic);
|
||||
assert_eq!(AuthStyle::OpenhumanJwt.as_str(), "openhuman_jwt");
|
||||
assert_eq!(AuthStyle::Anthropic.as_str(), "anthropic");
|
||||
assert_eq!(AuthStyle::None.as_str(), "none");
|
||||
|
||||
@@ -554,6 +554,7 @@ async fn thread_ops_welcome_migration_and_turn_state_cover_error_and_cleanup_pat
|
||||
dedicated_thread: true,
|
||||
prompt_chars: 42,
|
||||
worker_thread_id: None,
|
||||
display_name: None,
|
||||
}));
|
||||
assert!(mirror.observe(&AgentProgress::SubagentCompleted {
|
||||
agent_id: "researcher".into(),
|
||||
|
||||
@@ -3140,6 +3140,7 @@ fn turn_state_mirror_persists_progress_edges_from_public_events() {
|
||||
dedicated_thread: true,
|
||||
prompt_chars: 99,
|
||||
worker_thread_id: None,
|
||||
display_name: None,
|
||||
}));
|
||||
assert!(!mirror.observe(&AgentProgress::SubagentIterationStarted {
|
||||
agent_id: "researcher".into(),
|
||||
|
||||
Reference in New Issue
Block a user