Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d507a41
Refactor inference around external Ollama routing
senamakel May 16, 2026
481482e
Clarify external Ollama routing errors
senamakel May 16, 2026
56c89f6
Remove legacy Ollama management RPCs
senamakel May 16, 2026
8223b5c
Add direct runtime inference coverage
senamakel May 16, 2026
b1abf16
Expand local model UI coverage
senamakel May 16, 2026
12bed1a
test: polish direct runtime coverage
senamakel May 17, 2026
ead0aed
Merge remote-tracking branch 'upstream/main' into codex/inference-ext…
senamakel May 17, 2026
fe06af5
fix: address inference review follow-ups
senamakel May 17, 2026
febf2fa
chore: apply rustfmt review follow-ups
senamakel May 17, 2026
4bf6ea6
Remove GIF and Tenor local AI flows
senamakel May 17, 2026
97facae
Remove unused Tenor backend search helper
senamakel May 17, 2026
9c89f06
chore: apply module ordering format
senamakel May 17, 2026
8464d55
Move inference RPCs out of local_ai namespace
senamakel May 17, 2026
1bfa13e
Move inference management RPCs into inference
senamakel May 17, 2026
bfc3885
refactor(inference): unify all inference concerns under src/openhuman…
senamakel May 17, 2026
2eab968
feat(inference): per-model temperature toggle, provider E2E tests, do…
senamakel May 17, 2026
acaf8b5
chore: apply prettier auto-fix on rpcMethods test
senamakel May 17, 2026
c685081
fix: address CodeRabbit review on PR #1975
senamakel May 17, 2026
a66ecbd
Merge remote-tracking branch 'upstream/main' into codex/inference-ext…
senamakel May 17, 2026
4a006b6
Merge remote-tracking branch 'upstream/main' into codex/inference-ext…
senamakel May 17, 2026
9b8fcff
fix(inference): unwrap diagnostics RpcOutcome + fix rpcMethods drift-…
senamakel May 17, 2026
0b92c50
fix(inference): refresh provider field on status snapshot from curren…
senamakel May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ rppal = { version = "0.22", optional = true }
# crates we never use (and that bloat the dev Cargo.lock noticeably).
# TestTransport only needs the `test` feature.
sentry = { version = "0.47.0", default-features = false, features = ["test"] }
# Mock HTTP server for provider E2E tests (inference_provider_e2e).
wiremock = "0.6"

[features]
sandbox-landlock = ["dep:landlock"]
Expand Down
72 changes: 8 additions & 64 deletions app/src/components/settings/panels/LocalModelDebugPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@ import {
type LocalAiTtsResult,
openhumanLocalAiAssetsStatus,
openhumanLocalAiDiagnostics,
openhumanLocalAiDownload,
openhumanLocalAiDownloadAllAssets,
openhumanLocalAiDownloadAsset,
openhumanLocalAiDownloadsProgress,
openhumanLocalAiEmbed,
openhumanLocalAiPrompt,
openhumanLocalAiSetOllamaPath,
openhumanLocalAiStatus,
openhumanLocalAiSummarize,
openhumanLocalAiTranscribe,
Expand Down Expand Up @@ -60,8 +57,6 @@ const LocalModelDebugPanel = () => {
const [assets, setAssets] = useState<LocalAiAssetsStatus | null>(null);
const [downloads, setDownloads] = useState<LocalAiDownloadsProgress | null>(null);
const [statusError, setStatusError] = useState<string>('');
const [isTriggeringDownload, setIsTriggeringDownload] = useState(false);
const [bootstrapMessage, setBootstrapMessage] = useState<string>('');
const [assetDownloadBusy, setAssetDownloadBusy] = useState<Record<string, boolean>>({});

const [summaryInput, setSummaryInput] = useState('');
Expand Down Expand Up @@ -97,8 +92,6 @@ const LocalModelDebugPanel = () => {
const [diagnosticsError, setDiagnosticsError] = useState('');

const [showErrorDetail, setShowErrorDetail] = useState(false);
const [ollamaPathInput, setOllamaPathInput] = useState('');
const [isSettingPath, setIsSettingPath] = useState(false);

const progress = useMemo(() => {
const downloadProgress = progressFromDownloads(downloads);
Expand Down Expand Up @@ -158,29 +151,6 @@ const LocalModelDebugPanel = () => {
};
}, []);

const triggerDownload = async (force: boolean) => {
if (!runtimeEnabled) return;
setIsTriggeringDownload(true);
setStatusError('');
setBootstrapMessage('');
try {
await openhumanLocalAiDownload(force);
await openhumanLocalAiDownloadAllAssets(force);
const freshStatus = await openhumanLocalAiStatus();
setStatus(freshStatus.result);
if (freshStatus.result?.state === 'ready') {
setBootstrapMessage(force ? 'Re-bootstrap complete' : 'Models verified');
}
setTimeout(() => setBootstrapMessage(''), 3000);
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to trigger local model bootstrap';
setStatusError(message);
} finally {
setIsTriggeringDownload(false);
}
};

const runSummaryTest = async () => {
if (!runtimeEnabled || !summaryInput.trim()) return;
setIsSummaryLoading(true);
Expand Down Expand Up @@ -305,32 +275,6 @@ const LocalModelDebugPanel = () => {
}
};

const handleSetOllamaPath = async () => {
setIsSettingPath(true);
setStatusError('');
try {
await openhumanLocalAiSetOllamaPath(ollamaPathInput);
await loadStatus();
} catch (err) {
setStatusError(err instanceof Error ? err.message : 'Failed to set Ollama path');
} finally {
setIsSettingPath(false);
}
};

const handleClearOllamaPath = async () => {
setOllamaPathInput('');
setIsSettingPath(true);
try {
await openhumanLocalAiSetOllamaPath('');
await loadStatus();
} catch (err) {
setStatusError(err instanceof Error ? err.message : 'Failed to clear Ollama path');
} finally {
setIsSettingPath(false);
}
};

const handleRunDiagnostics = async () => {
setIsDiagnosticsLoading(true);
setDiagnosticsError('');
Expand Down Expand Up @@ -361,25 +305,25 @@ const LocalModelDebugPanel = () => {
isDiagnosticsLoading={isDiagnosticsLoading}
diagnosticsError={diagnosticsError}
statusError={statusError}
isTriggeringDownload={isTriggeringDownload}
bootstrapMessage={bootstrapMessage}
isTriggeringDownload={false}
bootstrapMessage=""
progress={progress}
isIndeterminateDownload={isIndeterminateDownload}
isInstalling={isInstalling}
isInstallError={isInstallError}
showErrorDetail={showErrorDetail}
ollamaPathInput={ollamaPathInput}
isSettingPath={isSettingPath}
ollamaPathInput=""
isSettingPath={false}
downloadedText={downloadedText}
speedText={speedText}
etaText={etaText}
statusTone={statusTone}
runtimeEnabled={runtimeEnabled}
onRefreshStatus={() => void loadStatus()}
onTriggerDownload={force => void triggerDownload(force)}
onSetOllamaPath={() => void handleSetOllamaPath()}
onClearOllamaPath={() => void handleClearOllamaPath()}
onSetOllamaPathInput={setOllamaPathInput}
onTriggerDownload={() => {}}
onSetOllamaPath={() => {}}
onClearOllamaPath={() => {}}
onSetOllamaPathInput={() => {}}
onToggleErrorDetail={() => setShowErrorDetail(v => !v)}
onRunDiagnostics={() => void handleRunDiagnostics()}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import DeviceCapabilitySection from './DeviceCapabilitySection';

const mockApplyPreset = vi.fn();

vi.mock('../../../../utils/tauriCommands', () => ({
openhumanLocalAiApplyPreset: (...args: unknown[]) => mockApplyPreset(...args),
}));

const makePresetsData = (overrides: Record<string, unknown> = {}) => ({
presets: [
{
tier: 'ram_2_4gb',
label: '2-4 GB',
description: 'Small local tier',
chat_model_id: 'gemma3:1b-it-qat',
vision_model_id: '',
embedding_model_id: 'bge-m3',
quantization: 'q4',
vision_mode: 'disabled',
supports_screen_summary: false,
target_ram_gb: 4,
min_ram_gb: 2,
approx_download_gb: 1.2,
},
],
recommended_tier: 'ram_2_4gb',
current_tier: 'ram_2_4gb',
selected_tier: 'ram_2_4gb',
recommend_disabled: false,
local_ai_enabled: true,
device: {
total_ram_bytes: 16 * 1024 * 1024 * 1024,
cpu_count: 8,
cpu_brand: 'Test CPU',
os_name: 'macOS',
os_version: '15',
has_gpu: true,
gpu_description: 'Test GPU',
},
...overrides,
});

describe('DeviceCapabilitySection', () => {
beforeEach(() => {
mockApplyPreset.mockReset();
});

it('renders external runtime guidance when ollama is unavailable', () => {
render(
<DeviceCapabilitySection
presetsData={makePresetsData()}
presetsLoading={false}
presetError=""
presetSuccess={null}
formatRamGb={() => '16 GB'}
ollamaAvailable={false}
/>
);

expect(screen.getByText(/Run Ollama first/i)).toBeTruthy();
expect(screen.getByRole('link', { name: 'Ollama docs' })).toBeTruthy();
expect(screen.getByTitle('Run Ollama first to use this tier')).toBeTruthy();
});

it('allows selecting the disabled cloud fallback tier', async () => {
mockApplyPreset.mockResolvedValueOnce({ applied_tier: 'disabled' });

render(
<DeviceCapabilitySection
presetsData={makePresetsData({ local_ai_enabled: false })}
presetsLoading={false}
presetError=""
presetSuccess={null}
formatRamGb={() => '16 GB'}
/>
);

fireEvent.click(screen.getByRole('button', { name: /Disabled.*0 GB/i }));

await waitFor(() => {
expect(mockApplyPreset).toHaveBeenCalledWith('disabled');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,15 @@ interface DeviceCapabilitySectionProps {
formatRamGb: (bytes: number) => string;
onPresetApplied?: (result: ApplyPresetResult) => void;
/**
* When `false`, the Ollama runtime isn't installed yet. Local tiers
* require Ollama, so they're rendered disabled with a notice that
* lets the user install Ollama in place. The "Disabled (cloud
* fallback)" option stays enabled since it doesn't need Ollama.
* When `false`, the external Ollama runtime isn't reachable yet. Local tiers
* stay disabled until the user runs Ollama themselves. The "Disabled (cloud
* fallback)" option stays enabled since it doesn't depend on Ollama.
*/
ollamaAvailable?: boolean;
/**
* Triggers the same install pipeline the Runtime Status section uses.
* Wired only when `ollamaAvailable === false` to surface an inline
* Install Ollama button next to the locked tiers.
*/
onTriggerOllamaInstall?: () => void;
/** True while an install pipeline is already running. */
isTriggeringInstall?: boolean;
/**
* Live state from `local_ai_status` so the notice can show real install
* progress: `installing`, `downloading`, `degraded`, etc. The button's
* own `isTriggeringInstall` only covers the RPC round-trip (~ms);
* `installState` covers the entire backend pipeline (~60s).
*/
installState?: string;
/** Latest `status.warning` text — shown under the progress label. */
installWarning?: string | null;
/** Latest `status.error_detail` — shown when state is `degraded`. */
installError?: string | null;
}

Expand All @@ -57,9 +42,13 @@ const DeviceCapabilitySection = ({
installWarning,
installError,
}: DeviceCapabilitySectionProps) => {
const installInProgress =
installState === 'installing' || installState === 'downloading' || installState === 'loading';
const installFailed = installState === 'degraded';
void onTriggerOllamaInstall;
void isTriggeringInstall;
void installState;
void installWarning;
void installError;
const installInProgress = false;
const installFailed = false;
const [applying, setApplying] = useState<string | null>(null);
const [applyError, setApplyError] = useState<string>('');
const [applySuccess, setApplySuccess] = useState<ApplyPresetResult | null>(null);
Expand Down Expand Up @@ -187,26 +176,18 @@ const DeviceCapabilitySection = ({
) : (
<>
<div className="text-xs text-amber-800">
<span className="font-semibold text-amber-900">Install Ollama first.</span> Local
tiers run on the Ollama runtime, which isn&apos;t installed yet. The &ldquo;Disabled
(cloud fallback)&rdquo; option stays available either way.
<span className="font-semibold text-amber-900">Run Ollama first.</span> Local tiers
depend on an externally managed Ollama endpoint. Start it yourself, pull the models
you want, and keep using &ldquo;Disabled (cloud fallback)&rdquo; until the runtime
is reachable.
</div>
<div className="flex items-center gap-2">
{onTriggerOllamaInstall && (
<button
type="button"
onClick={onTriggerOllamaInstall}
disabled={isTriggeringInstall}
className="px-3 py-1.5 text-xs rounded-md bg-amber-600 hover:bg-amber-700 disabled:opacity-60 text-white font-medium">
{isTriggeringInstall ? 'Starting…' : 'Install Ollama'}
</button>
)}
<a
href="https://ollama.com"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 text-xs rounded-md border border-amber-300 hover:border-amber-400 text-amber-800">
Install manually
Ollama docs
</a>
</div>
</>
Expand Down Expand Up @@ -257,7 +238,7 @@ const DeviceCapabilitySection = ({
key={preset.tier}
onClick={() => void handleApply(preset.tier)}
disabled={applying !== null || locked}
title={locked ? 'Install Ollama first to use this tier' : undefined}
title={locked ? 'Run Ollama first to use this tier' : undefined}
className={`w-full text-left rounded-lg border p-3 transition-colors ${
isCurrent
? 'border-primary-400 bg-primary-50'
Expand Down
Loading
Loading