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:
Jwalin Shah
2026-04-21 23:15:09 -07:00
committed by GitHub
co-authored by Jwalin Shah WOZCODE Steven Enamakel
parent e405e2cc99
commit 6a28e75e94
9 changed files with 81 additions and 18 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "OpenHuman"
version = "0.52.27"
version = "0.52.28"
dependencies = [
"anyhow",
"async-trait",
+11
View File
@@ -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>
+1 -1
View File
@@ -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")]
+2 -1
View File
@@ -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!(
+1 -5
View File
@@ -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
+11 -2
View File
@@ -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" \
+1 -1
View File
@@ -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;
+46 -6
View File
@@ -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,
+7 -1
View File
@@ -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,
}