mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(routing): pluggable local inference (MLX, llama.cpp, LM Studio) + openhuman:// deep-link + OpenSSL 3 PKCS12 fix (#750)
* feat(routing,local-ai): pluggable local inference + openhuman:// deep-link
Salvaged from recovery commit cfb1fd9f (Apr 19) — originally auto-backed up
by claude-mem recovery tool, not yet PR'd.
## Local AI routing becomes pluggable (factory.rs, health.rs)
Previously the IntelligentRoutingProvider hard-wired Ollama as the only local
inference backend. This commit lets operators point at any OpenAI-compatible
local server (llama.cpp / llama-server, LM Studio, MLX's mlx_lm.server /
mlx-omni-server, vLLM — anything exposing /v1/chat/completions + /v1/models).
Config surface:
- `OPENHUMAN_LOCAL_INFERENCE_URL` env var — full /v1 base URL. When set,
health is probed via GET {base}/models (OpenAI-compat) instead of Ollama's
/api/tags.
- `LocalAiConfig.provider` accepts "llamacpp" or "llama-server" as aliases
that default to http://127.0.0.1:8080/v1.
- Default (no env, no override) stays Ollama — zero config change for
existing users.
Motivation: Ollama's embedded llama.cpp cannot yet load Gemma 4 E2B and
other recent models; MLX is significantly faster on Apple Silicon.
Operators should be able to pick the backend; end users see nothing.
## Deep-link scheme (Info.plist)
Adds CFBundleURLTypes entry registering `openhuman://` for the macOS bundle
so browser-issued deep links (e.g. openhuman://auth?token=...) launch the
installed app. See .claude/rules/14-deep-link-platform-guide.md for the
flow — requires a .app bundle (not `tauri dev`).
## OpenSSL 3.x PKCS12 fix (setup-dev-codesign.sh)
Adds `-legacy` to the `openssl pkcs12 -export` call. Required on OpenSSL 3.x
because the modern SHA-256 MAC / AES-256-CBC defaults are not yet supported
by macOS `security` tool, which silently fails to import the cert.
Notes:
- Dropped the CLAUDE.md.new noise and the mic-description copy downgrade
from the original recovery commit; kept only the load-bearing changes.
- Cargo.lock files reset to upstream/main to avoid dependency drift — the
new code does not introduce new deps.
Co-Authored-By: WOZCODE <contact@withwoz.com>
* fix(routing,codesign): address CodeRabbit review on PR #750
- setup-dev-codesign.sh: probe for openssl `-legacy` support before
using it so older OpenSSL/LibreSSL installs don't silently fail;
drop the stderr suppression on pkcs12 so import errors surface.
- routing/factory.rs: trim provider before alias matching, lower the
local-inference diagnostic to `debug` and drop raw URLs from the
log fields, and honor `OPENHUMAN_OLLAMA_BASE_URL` via the existing
`ollama_base_url()` helper in the default Ollama branch.
- local_ai/mod.rs: re-export `ollama_base_url` for the routing factory.
* style(app/src-tauri): apply rustfmt to merged main
`cargo fmt --check` in the merge workspace flagged two hunks inherited
from upstream main:
- app/src-tauri/src/lib.rs: move `mod notification_settings;` between
the cfg(cef) imessage_scanner and slack_scanner declarations so the
ordering matches rustfmt's reordering output.
- app/src-tauri/src/webview_accounts/mod.rs: wrap the long
`try_state::<NotificationSettingsState>()` binding.
No behavior change.
* format
---------
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Jwalin Shah
WOZCODE
Steven Enamakel
parent
e405e2cc99
commit
6a28e75e94
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "OpenHuman"
|
||||
version = "0.52.27"
|
||||
version = "0.52.28"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
@@ -6,5 +6,16 @@
|
||||
<string>OpenHuman uses the microphone for voice dictation and for voice/video calls and huddles inside embedded apps (Google Meet, Discord, Slack).</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>OpenHuman uses the camera for video calls and huddles inside embedded apps (Google Meet, Discord, Slack).</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.openhuman.app</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>openhuman</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,11 +3,11 @@ compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supp
|
||||
|
||||
mod core_process;
|
||||
mod core_update;
|
||||
mod notification_settings;
|
||||
#[cfg(feature = "cef")]
|
||||
mod discord_scanner;
|
||||
#[cfg(feature = "cef")]
|
||||
mod imessage_scanner;
|
||||
mod notification_settings;
|
||||
#[cfg(feature = "cef")]
|
||||
mod slack_scanner;
|
||||
#[cfg(feature = "cef")]
|
||||
|
||||
@@ -267,7 +267,8 @@ fn forward_native_notification<R: Runtime>(
|
||||
payload: &tauri_runtime_cef::notification::NotificationPayload,
|
||||
) {
|
||||
// Feature flag — bail early when the user hasn't opted in.
|
||||
if let Some(settings) = app.try_state::<crate::notification_settings::NotificationSettingsState>()
|
||||
if let Some(settings) =
|
||||
app.try_state::<crate::notification_settings::NotificationSettingsState>()
|
||||
{
|
||||
if !settings.enabled() {
|
||||
log::debug!(
|
||||
|
||||
@@ -39,10 +39,7 @@ import {
|
||||
openhumanVoiceTts,
|
||||
} from '../utils/tauriCommands';
|
||||
import { formatTimelineEntry } from '../utils/toolTimelineFormatting';
|
||||
import {
|
||||
AgentMessageBubble,
|
||||
BubbleMarkdown,
|
||||
} from './conversations/components/AgentMessageBubble';
|
||||
import { AgentMessageBubble, BubbleMarkdown } from './conversations/components/AgentMessageBubble';
|
||||
import { LimitPill } from './conversations/components/LimitPill';
|
||||
import { ToolTimelineBlock } from './conversations/components/ToolTimelineBlock';
|
||||
import {
|
||||
@@ -66,7 +63,6 @@ type ReplyMode = 'text' | 'voice';
|
||||
const AUTOCOMPLETE_POLL_DEBOUNCE_MS = 320;
|
||||
const AUTOCOMPLETE_MIN_CONTEXT_CHARS = 3;
|
||||
|
||||
|
||||
interface ConversationsProps {
|
||||
/**
|
||||
* `page` (default) renders the centered max-w-2xl card layout used as
|
||||
|
||||
@@ -65,13 +65,22 @@ openssl req \
|
||||
2>/dev/null
|
||||
|
||||
# ── Bundle to PKCS12 ─────────────────────────────────────────────────────────
|
||||
# `-legacy` keeps PKCS12 MAC/encryption compatible with macOS `security` tool
|
||||
# which does not yet support OpenSSL 3.x defaults (SHA256 MAC / AES-256-CBC).
|
||||
# Older OpenSSL/LibreSSL (including the macOS-bundled LibreSSL) do not know
|
||||
# about `-legacy`, so probe for support before adding it.
|
||||
PKCS12_LEGACY_ARGS=()
|
||||
if openssl pkcs12 -help 2>&1 | grep -q -- '-legacy'; then
|
||||
PKCS12_LEGACY_ARGS=(-legacy)
|
||||
fi
|
||||
|
||||
openssl pkcs12 \
|
||||
-export \
|
||||
"${PKCS12_LEGACY_ARGS[@]}" \
|
||||
-out "$P12" \
|
||||
-inkey "$KEY" \
|
||||
-in "$CERT" \
|
||||
-passout "pass:$P12_PASS" \
|
||||
2>/dev/null
|
||||
-passout "pass:$P12_PASS"
|
||||
|
||||
# ── Import into login Keychain ───────────────────────────────────────────────
|
||||
security import "$P12" \
|
||||
|
||||
@@ -15,7 +15,7 @@ pub mod sentiment;
|
||||
mod install;
|
||||
pub(crate) mod model_ids;
|
||||
mod ollama_api;
|
||||
pub(crate) use ollama_api::OLLAMA_BASE_URL;
|
||||
pub(crate) use ollama_api::{ollama_base_url, OLLAMA_BASE_URL};
|
||||
mod parse;
|
||||
pub(crate) mod paths;
|
||||
mod service;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::openhuman::config::LocalAiConfig;
|
||||
use crate::openhuman::local_ai::OLLAMA_BASE_URL;
|
||||
use crate::openhuman::local_ai::ollama_base_url;
|
||||
use crate::openhuman::providers::compatible::{AuthStyle, OpenAiCompatibleProvider};
|
||||
use crate::openhuman::providers::Provider;
|
||||
|
||||
use super::health::LocalHealthChecker;
|
||||
use super::provider::IntelligentRoutingProvider;
|
||||
|
||||
/// Cache TTL for the non-ollama local health probe. Mirrors the default used
|
||||
/// by [`LocalHealthChecker::new`].
|
||||
const LOCAL_HEALTH_TTL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Construct an [`IntelligentRoutingProvider`] from a remote backend provider
|
||||
/// and the local AI configuration.
|
||||
///
|
||||
@@ -22,16 +27,51 @@ pub fn new_provider(
|
||||
local_ai_config: &LocalAiConfig,
|
||||
remote_fallback_model: &str,
|
||||
) -> IntelligentRoutingProvider {
|
||||
let local_base = format!("{}/v1", OLLAMA_BASE_URL);
|
||||
// Allow operators to point the local routing tier at an OpenAI-compatible
|
||||
// server other than Ollama (e.g. llama-server for Gemma 4 E2B, which
|
||||
// Ollama's embedded llama.cpp cannot load yet as of April 2026).
|
||||
//
|
||||
// `OPENHUMAN_LOCAL_INFERENCE_URL` — full `/v1` base URL of the local
|
||||
// OpenAI-compat server. When set, health is probed via `GET {base}/models`
|
||||
// instead of Ollama's `/api/tags`.
|
||||
let override_base = std::env::var("OPENHUMAN_LOCAL_INFERENCE_URL")
|
||||
.ok()
|
||||
.map(|s| s.trim().trim_end_matches('/').to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
let provider_kind = local_ai_config.provider.trim().to_ascii_lowercase();
|
||||
let use_openai_compat_local =
|
||||
override_base.is_some() || matches!(provider_kind.as_str(), "llamacpp" | "llama-server");
|
||||
|
||||
let (provider_label, local_base, health) = if use_openai_compat_local {
|
||||
let base = override_base.unwrap_or_else(|| "http://127.0.0.1:8080/v1".to_string());
|
||||
let probe = format!("{base}/models");
|
||||
tracing::debug!(
|
||||
provider = %provider_kind,
|
||||
"[routing] local inference configured via OpenAI-compat (non-ollama)"
|
||||
);
|
||||
(
|
||||
"llamacpp",
|
||||
base,
|
||||
Arc::new(LocalHealthChecker::with_probe_url(probe, LOCAL_HEALTH_TTL)),
|
||||
)
|
||||
} else {
|
||||
let ollama_base = ollama_base_url();
|
||||
let local_v1 = format!("{ollama_base}/v1");
|
||||
(
|
||||
"ollama",
|
||||
local_v1,
|
||||
Arc::new(LocalHealthChecker::new(&ollama_base)),
|
||||
)
|
||||
};
|
||||
|
||||
let local: Box<dyn Provider> = Box::new(OpenAiCompatibleProvider::new(
|
||||
"ollama",
|
||||
provider_label,
|
||||
&local_base,
|
||||
None, // Ollama does not require authentication
|
||||
None, // local servers do not require authentication
|
||||
AuthStyle::Bearer,
|
||||
));
|
||||
|
||||
let health = Arc::new(LocalHealthChecker::new(OLLAMA_BASE_URL));
|
||||
|
||||
IntelligentRoutingProvider::new(
|
||||
remote,
|
||||
local,
|
||||
|
||||
@@ -45,6 +45,12 @@ impl LocalHealthChecker {
|
||||
|
||||
/// Create a checker with a custom cache TTL (useful in tests).
|
||||
pub fn with_ttl(base_url: &str, ttl: Duration) -> Self {
|
||||
Self::with_probe_url(format!("{base_url}/api/tags"), ttl)
|
||||
}
|
||||
|
||||
/// Create a checker with an explicit full probe URL (for non-ollama local
|
||||
/// backends such as llama-server, whose health endpoint is `/v1/models`).
|
||||
pub fn with_probe_url(probe_url: String, ttl: Duration) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(PROBE_TIMEOUT)
|
||||
.build()
|
||||
@@ -57,7 +63,7 @@ impl LocalHealthChecker {
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
probe_url: format!("{base_url}/api/tags"),
|
||||
probe_url,
|
||||
cache: Mutex::new(None),
|
||||
ttl,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user