From 4e221e978be7cf6bd3bb6de96dde00792a2c38d9 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Sat, 30 May 2026 22:37:06 -0700 Subject: [PATCH] Expand inference provider catalog (#3057) (#3062) --- .../components/settings/panels/AIPanel.tsx | 129 ++------- .../panels/__tests__/AIPanel.test.tsx | 76 +++++ .../__tests__/builtinCloudProviders.test.ts | 49 ++++ .../settings/panels/builtinCloudProviders.ts | 236 ++++++++++++++++ docs/inference-provider-catalog.md | 52 ++++ src/openhuman/about_app/catalog_data.rs | 4 +- .../config/schema/cloud_providers.rs | 265 ++++++++++++++++-- src/openhuman/inference/README.md | 77 ++--- .../threads/turn_state/mirror_tests.rs | 1 + .../config_auth_app_state_connectivity_e2e.rs | 20 ++ tests/memory_core_threads_raw_coverage_e2e.rs | 1 + tests/memory_threads_raw_coverage_e2e.rs | 1 + 12 files changed, 743 insertions(+), 168 deletions(-) create mode 100644 app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts create mode 100644 app/src/components/settings/panels/builtinCloudProviders.ts create mode 100644 docs/inference-provider-catalog.md diff --git a/app/src/components/settings/panels/AIPanel.tsx b/app/src/components/settings/panels/AIPanel.tsx index c721d47e9..e92b653d4 100644 --- a/app/src/components/settings/panels/AIPanel.tsx +++ b/app/src/components/settings/panels/AIPanel.tsx @@ -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 = { 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 => ( (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). diff --git a/app/src/components/settings/panels/__tests__/AIPanel.test.tsx b/app/src/components/settings/panels/__tests__/AIPanel.test.tsx index c960a1074..9c37cc741 100644 --- a/app/src/components/settings/panels/__tests__/AIPanel.test.tsx +++ b/app/src/components/settings/panels/__tests__/AIPanel.test.tsx @@ -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(); + + 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(); + + 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(); + + 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( diff --git a/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts b/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts new file mode 100644 index 000000000..63d1c6f55 --- /dev/null +++ b/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts @@ -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', + ]) + ); + }); +}); diff --git a/app/src/components/settings/panels/builtinCloudProviders.ts b/app/src/components/settings/panels/builtinCloudProviders.ts new file mode 100644 index 000000000..2ec0c32c0 --- /dev/null +++ b/app/src/components/settings/panels/builtinCloudProviders.ts @@ -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; + +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; +} diff --git a/docs/inference-provider-catalog.md b/docs/inference-provider-catalog.md new file mode 100644 index 000000000..612164232 --- /dev/null +++ b/docs/inference-provider-catalog.md @@ -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:`; +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. | diff --git a/src/openhuman/about_app/catalog_data.rs b/src/openhuman/about_app/catalog_data.rs index 4a28b5121..5b1c1b938 100644 --- a/src/openhuman/about_app/catalog_data.rs +++ b/src/openhuman/about_app/catalog_data.rs @@ -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, }, diff --git a/src/openhuman/config/schema/cloud_providers.rs b/src/openhuman/config/schema/cloud_providers.rs index 87b215742..0ec08375a 100644 --- a/src/openhuman/config/schema/cloud_providers.rs +++ b/src/openhuman/config/schema/cloud_providers.rs @@ -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 + ); + } + } } diff --git a/src/openhuman/inference/README.md b/src/openhuman/inference/README.md index 0806db380..d74e74bf6 100644 --- a/src/openhuman/inference/README.md +++ b/src/openhuman/inference/README.md @@ -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:` 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`; 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`; 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 diff --git a/src/openhuman/threads/turn_state/mirror_tests.rs b/src/openhuman/threads/turn_state/mirror_tests.rs index 909a0fdc7..ee0a7cc80 100644 --- a/src/openhuman/threads/turn_state/mirror_tests.rs +++ b/src/openhuman/threads/turn_state/mirror_tests.rs @@ -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")); diff --git a/tests/config_auth_app_state_connectivity_e2e.rs b/tests/config_auth_app_state_connectivity_e2e.rs index ae2add4d0..3d1eaadd5 100644 --- a/tests/config_auth_app_state_connectivity_e2e.rs +++ b/tests/config_auth_app_state_connectivity_e2e.rs @@ -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"); diff --git a/tests/memory_core_threads_raw_coverage_e2e.rs b/tests/memory_core_threads_raw_coverage_e2e.rs index 9701ce6be..cd4ca6a95 100644 --- a/tests/memory_core_threads_raw_coverage_e2e.rs +++ b/tests/memory_core_threads_raw_coverage_e2e.rs @@ -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(), diff --git a/tests/memory_threads_raw_coverage_e2e.rs b/tests/memory_threads_raw_coverage_e2e.rs index 33e034048..912228936 100644 --- a/tests/memory_threads_raw_coverage_e2e.rs +++ b/tests/memory_threads_raw_coverage_e2e.rs @@ -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(),