Skip to content

feat: add API key support for custom OpenAI-compatible providers and …#905

Closed
unn-Known1 wants to merge 1 commit into
tinyhumansai:mainfrom
unn-Known1:main
Closed

feat: add API key support for custom OpenAI-compatible providers and …#905
unn-Known1 wants to merge 1 commit into
tinyhumansai:mainfrom
unn-Known1:main

Conversation

@unn-Known1
Copy link
Copy Markdown
Contributor

@unn-Known1 unn-Known1 commented Apr 24, 2026

…integrate into configuration workflows

Summary

  • What changed and why.
  • Keep this to 3-6 bullets focused on user-visible or architecture-impacting changes.

Problem

  • What issue or risk this PR addresses.
  • Include context needed for reviewers to evaluate correctness quickly.

Solution

  • How the implementation solves the problem.
  • Note important design decisions and tradeoffs.

Submission Checklist

  • Unit tests — Vitest (app/) and/or cargo test (core) for logic you add or change
  • E2E / integration — Where behavior is user-visible or crosses UI → Tauri → sidecar → JSON-RPC; use existing harnesses (app/test/e2e, mock backend, tests/json_rpc_e2e.rs as appropriate)
  • N/A — If truly not applicable, say why (e.g. change is documentation-only)
  • Doc comments/// / //! (Rust), JSDoc or brief file/module headers (TS) on public APIs and non-obvious modules
  • Inline comments — Where logic, invariants, or edge cases aren’t clear from names alone (keep them grep-friendly; avoid restating the code)

(Any feature related checklist can go in here)

Impact

  • Runtime/platform impact (desktop/mobile/web/CLI), if any.
  • Performance, security, migration, or compatibility implications.

Related

  • Closes:
  • Follow-up PR(s)/TODOs:

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a new settings section for configuring custom OpenAI-compatible model backends with API key support
    • Users can now set and manage authentication credentials for custom model endpoints
    • Configuration is automatically saved and restored when the application restarts

@unn-Known1 unn-Known1 requested a review from a team April 24, 2026 21:23
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

This PR adds API key authentication support for custom OpenAI-compatible model backends. It introduces a new UI component for configuring base URL and API key, extends the configuration schema to persist credentials, and threads the API key parameter through provider factory functions and all construction callsites.

Changes

Cohort / File(s) Summary
Frontend UI & Configuration
app/src/components/settings/panels/LocalModelDebugPanel.tsx, app/src/components/settings/panels/local-model/CustomModelSection.tsx, app/src/utils/tauriCommands/config.ts
New CustomModelSection component provides inputs for custom OpenAI-compatible backend (base URL and API key), fetches existing config on mount, handles save/error states, and persists via openhumanUpdateModelSettings Tauri command. LocalModelDebugPanel now renders this component. TypeScript ModelSettingsUpdate interface extended with api_key field.
Configuration Schema & Persistence
src/openhuman/config/schema/types.rs, src/openhuman/config/schema/local_ai.rs, src/openhuman/config/ops.rs, src/openhuman/config/schemas.rs
Top-level Config and LocalAiConfig now include optional api_key and base_url fields (defaulting to None). ModelSettingsUpdate struct made public with public fields and Clone derive. apply_model_settings updated to conditionally write config.api_key, treating empty/whitespace strings as clear requests (sets to None), and persisting afterwards. Update handler switched to inline JSON deserialization to capture api_key field.
Provider Factory Core
src/openhuman/providers/ops.rs, src/openhuman/routing/factory.rs
Six provider factory function signatures extended to accept api_key: Option<&str> parameter. create_backend_inference_provider now selects "custom_openai" Bearer-auth provider when both URL and key are provided, falling back to existing OpenHumanBackendProvider. Local routing factory recognizes custom_openai backend type, applies fallback logic to local_ai_config.base_url, passes API key to provider constructor, and yields appropriate label.
Provider Construction Callsites
src/openhuman/agent/harness/session/builder.rs, src/openhuman/agent/triage/routing.rs, src/openhuman/channels/routes.rs, src/openhuman/channels/runtime/startup.rs, src/openhuman/learning/linkedin_enrichment.rs, src/openhuman/local_ai/ops.rs, src/openhuman/threads/ops.rs, src/openhuman/tools/impl/agent/delegate.rs
All provider factory callsites updated to supply config.api_key.as_deref() (or None where appropriate) as second parameter to factory functions. Preserves existing control flow and error handling; only extends argument list.
Development Artifact
scratch.js
New file importing callCoreRpc with note about relying on Rust sidecar/CLI or HTTP mock server instead of direct Node.js TypeScript execution.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CustomModelSection as CustomModelSection<br/>(UI)
    participant TauriCmd as Tauri Command Handler
    participant ConfigOps as Config Ops
    participant ProviderFactory as Provider Factory
    participant LocalProvider as Custom OpenAI<br/>Provider

    User->>CustomModelSection: Enter base_url & api_key
    CustomModelSection->>CustomModelSection: Trim values, clear errors
    CustomModelSection->>TauriCmd: openhumanUpdateModelSettings(api_url, api_key)
    TauriCmd->>ConfigOps: apply_model_settings(patch)
    ConfigOps->>ConfigOps: Update config.api_key<br/>(None if empty)
    ConfigOps->>ConfigOps: Persist snapshot
    TauriCmd->>CustomModelSection: Success response
    CustomModelSection->>CustomModelSection: Show success, auto-clear after 3s

    Note over User,LocalProvider: Subsequent LLM interaction
    User->>LocalProvider: Request inference
    LocalProvider->>ProviderFactory: create_intelligent_routing_provider<br/>(api_url, api_key, config, options)
    ProviderFactory->>ProviderFactory: Select "custom_openai" backend<br/>if both url & key present
    ProviderFactory->>LocalProvider: Initialize with Bearer auth
    LocalProvider->>LocalProvider: Use API key for requests
    LocalProvider-->>User: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Prune obsolete config API keys #739 — Removes API key plumbing in opposite direction; conflicts with this PR's additions to the same provider factory and configuration callsites.
  • Feat: smart routing #569 — Introduces intelligent routing provider and provider factory infrastructure that this PR extends with API key threading through the same functions and callsites.

Suggested reviewers

  • senamakel

Poem

🐰 hops with delight
With API keys tucked in Config's care,
Bearer tokens float through the air,
Custom backends now authenticate,
Let local models innovate! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding API key support for custom OpenAI-compatible providers. It directly reflects the core changes across the codebase (schema, UI, provider factory functions, and integration points).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
scratch.js (1)

1-3: ⚠️ Potential issue | 🔴 Critical

Remove scratch file from the PR.

This appears to be a development/debug scratch file that was accidentally committed. The file has no functionality beyond an import and an exploratory comment, and it's not related to the PR's stated objective of adding API key support for OpenAI-compatible providers.

Additionally, the import path ./app/src/services/coreRpcClient.ts won't work correctly from the repository root in a JavaScript file.

🗑️ Proposed fix

Delete this file entirely, or if it's needed for local development, add it to .gitignore:

+# Local development scratch files
+scratch.js
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scratch.js` around lines 1 - 3, Remove the accidental development scratch
file scratch.js from the PR; delete the file (or if you want to keep it for
personal local experiments, move it out of the repo and add its path to
.gitignore) and ensure any stray import statements like callCoreRpc from
./app/src/services/coreRpcClient.ts are not left in the tree because that
relative TS import is invalid from a JS file at the repo root.
src/openhuman/tools/impl/agent/delegate.rs (1)

187-198: ⚠️ Potential issue | 🟠 Major

Delegate tool passes None for api_key, breaking authentication for custom OpenAI-compatible backends.

The DelegateTool struct has no api_key field and its constructors don't accept one. At line 187–189, execute() calls create_backend_inference_provider(None, None, ...), hardcoding a null api_key. Every other call site (triage/routing, linkedin_enrichment) correctly passes config.api_key.as_deref(). Any user configuring a custom backend with required authentication will see delegation fail with auth errors while non-delegate inference succeeds.

Add an api_key: Option<String> field to DelegateTool, thread it through new_with_options() and with_depth_and_options() constructors, and pass self.api_key.as_deref() to the provider factory at line 188. Update the caller in tools/ops.rs to extract and forward the api_key from available config context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/tools/impl/agent/delegate.rs` around lines 187 - 198, Add an
api_key: Option<String> field to DelegateTool and thread it through its
constructors (new_with_options and with_depth_and_options), ensuring callers set
it from the config context in tools/ops.rs; then replace the hardcoded None in
execute() when calling providers::create_backend_inference_provider with
self.api_key.as_deref() so the provider factory receives the configured API key;
ensure any constructor signatures and the call site in tools/ops.rs are updated
to accept and forward the api_key from the available config.
src/openhuman/routing/factory.rs (1)

43-74: ⚠️ Potential issue | 🟡 Minor

Run cargo fmt on this hunk before merge.

CI is already failing cargo fmt --all -- --check here on the new matches! / if-else formatting, so this will stay red until the file is reformatted.

As per coding guidelines, "**/*.rs: Run cargo fmt and cargo check pre-merge for Rust files at both repo root (Cargo.toml) and app/src-tauri/Cargo.toml" and "**/*.rs: Rust code must follow cargo fmt formatting standards before merging."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/routing/factory.rs` around lines 43 - 74, Run rustfmt/cargo fmt
on this hunk to fix formatting around the matches! and if/else block;
specifically reformat the code that computes use_openai_compat_local and the
subsequent let (provider_label, local_base, health) = ... expression (which
references override_base, provider_kind, local_ai_config, ollama_base,
LocalHealthChecker) so it matches cargo fmt style. Execute `cargo fmt --all` (or
rustfmt on src/openhuman/routing/factory.rs) and recommit the changed file; no
logical changes required, only formatting.
src/openhuman/config/ops.rs (1)

228-246: ⚠️ Potential issue | 🟠 Major

Do not echo the stored API key back through config snapshots.

This now persists config.api_key and immediately returns snapshot_config_json(config), so both update_model_settings and the existing config snapshot flow can expose the raw credential to RPC consumers. The new UI already hydrates result.config.api_key back into renderer state, which turns this into a full secret round-trip. Please keep API keys write-only here and return only a redacted/configured signal; if the UI still needs “unchanged vs clear vs replace” behavior, the patch contract needs tri-state semantics instead of relying on reading the saved key back.

Based on learnings, "New or changed behavior must be accompanied by matching rustdoc or code comments. Update AGENTS.md or architecture docs when rules or user-visible behavior change." Also, as per coding guidelines, "Never log secrets, raw JWTs, API keys, credentials, or full PII; redact or omit sensitive fields."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/config/ops.rs` around lines 228 - 246, The current code saves
the real API key into config and then calls snapshot_config_json(config), which
returns the raw secret; instead, after persisting via config.save() keep the
real key in storage but create a sanitized copy for snapshots: clone the config
(or construct a sanitized Config), clear or replace config.api_key on that copy
(e.g., set sanitized.api_key = None or Some("[REDACTED]")), and pass the
sanitized copy to snapshot_config_json instead of the real config; reference
update.api_key, config.api_key, config.save(), and snapshot_config_json in your
changes.
src/openhuman/providers/ops.rs (1)

331-343: ⚠️ Potential issue | 🟠 Major

Add a unit test for the new api_key branch.

factory_backend only exercises the (None, None) path, which routes to the existing OpenHumanBackendProvider. The new OpenAI-compatible branch (both api_url and api_key set) is entirely uncovered, so a future regression that swaps the arms or changes the provider id / auth style would not be caught. Please add at least one test that constructs the provider with both args populated and asserts success (and ideally asserts the returned provider's id / type when reachable).

🧪 Proposed additional test
 #[cfg(test)]
 mod tests {
     use super::*;
 
     #[test]
     fn factory_backend() {
         assert!(create_backend_inference_provider(
             None,
             None,
             &ProviderRuntimeOptions::default()
         )
         .is_ok());
     }
+
+    #[test]
+    fn factory_backend_custom_openai_compatible() {
+        // Both api_url and api_key set → should take the OpenAI-compatible branch.
+        assert!(create_backend_inference_provider(
+            Some("https://example.invalid/v1"),
+            Some("test-key"),
+            &ProviderRuntimeOptions::default(),
+        )
+        .is_ok());
+    }
+
+    #[test]
+    fn factory_backend_api_key_without_url_falls_back() {
+        // Key without URL must not panic and must return a provider (fallback).
+        assert!(create_backend_inference_provider(
+            None,
+            Some("test-key"),
+            &ProviderRuntimeOptions::default(),
+        )
+        .is_ok());
+    }
 }

As per coding guidelines: "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/providers/ops.rs` around lines 331 - 343, Add a unit test that
covers the new OpenAI-compatible branch by calling
create_backend_inference_provider with both Some(api_url) and Some(api_key) (and
a ProviderRuntimeOptions::default()) and asserting the Result is Ok;
additionally, downcast or inspect the returned provider to verify its
identity/type or id matches the OpenAI-compatible provider (reference
create_backend_inference_provider and ProviderRuntimeOptions to locate the
factory and the provider type/id used in the new branch), ensuring the test
lives alongside the existing factory_backend test in the #[cfg(test)] mod.
🧹 Nitpick comments (1)
src/openhuman/providers/ops.rs (1)

156-162: Consider promoting the hardcoded "custom_openai" id to a constant.

Both the provider id string and the AuthStyle::Bearer choice are inlined here. Since INFERENCE_BACKEND_ID is already defined at Line 8 for the OpenHuman backend, defining a sibling pub const CUSTOM_OPENAI_BACKEND_ID: &str = "custom_openai"; would keep the two ids colocated and make future references (metrics, routing tables, fallback lookups) grep-friendly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/providers/ops.rs` around lines 156 - 162, Introduce a sibling
public constant for the hardcoded provider id and use it in the
OpenAiCompatibleProvider instantiation: add pub const CUSTOM_OPENAI_BACKEND_ID:
&str = "custom_openai"; near the existing INFERENCE_BACKEND_ID, then replace the
inline "custom_openai" in the call to OpenAiCompatibleProvider::new with
CUSTOM_OPENAI_BACKEND_ID; keep the AuthStyle::Bearer usage as-is but ensure the
new constant is exported so other modules (metrics, routing, fallbacks) can
reference it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/settings/panels/local-model/CustomModelSection.tsx`:
- Around line 7-112: Add a colocated Vitest file CustomModelSection.test.tsx
that imports CustomModelSection and tests its key behaviors: mock
openhumanGetConfig to return initial config and assert that apiUrl/apiKey are
populated on mount and loading state toggles; mock openhumanGetConfig to throw
and assert error display; test handleSave by mocking
openhumanUpdateModelSettings and asserting that blank inputs are sent as null
(normalization), that isSaving disables the button and that the transient
saveSuccess message appears then disappears (use fake timers to advance 3s);
also test save failure by having openhumanUpdateModelSettings reject and
asserting the error message is shown; use vi.mock to stub
openhumanGetConfig/openhumanUpdateModelSettings and `@testing-library/react` for
render/userEvent queries.

In `@app/src/components/settings/panels/LocalModelDebugPanel.tsx`:
- Around line 36-38: Imports from ./local-model are not alphabetized and there's
an extra blank line in the JSX which causes Prettier to fail; reorder the
imports so ModelDownloadSection, ModelStatusSection, CustomModelSection are
alphabetically sorted (place CustomModelSection in the correct spot), remove the
stray blank line before the new JSX element near the bottom of
LocalModelDebugPanel.tsx, then run pnpm prettier --write on the file (and
optionally tsc --noEmit in app/) to ensure style/type checks pass.

In `@src/openhuman/config/schema/local_ai.rs`:
- Around line 14-15: LocalAiConfig currently derives Debug and exposes the
sensitive api_key field; implement a custom Debug impl for LocalAiConfig (or
remove Debug from the derive) so that api_key is redacted (e.g., always print
api_key: "***") instead of its raw value; update the impl to reference the
struct name LocalAiConfig and the api_key field so any debug printing (for
example via tracing::debug!(?config.local_ai)) will not leak the API key.

In `@src/openhuman/providers/ops.rs`:
- Around line 153-176: Formatting in create_resilient_provider and surrounding
calls (notably the OpenAiCompatibleProvider::new call and the
create_resilient_provider_with_options invocation) is off and failing CI; run
cargo fmt across the workspace (including all changed Rust files and workspace
Cargo.toml manifests) and reformat the code so create_resilient_provider,
create_resilient_provider_with_options, OpenAiCompatibleProvider::new, and
ProviderRuntimeOptions::default usages follow rustfmt styling, then re-run the
formatter check before pushing.
- Around line 150-168: Update the create_backend_inference_provider doc comment
to reflect that providing api_key + api_url enables a Bearer-authenticated
OpenAI-compatible custom endpoint; ensure ProviderRuntimeOptions is not silently
ignored on the custom branch by either documenting that relevant fields (e.g.
reasoning_enabled, auth_profile_override) are not used or by threading required
fields into OpenAiCompatibleProvider::new; add a tracing::warn! when exactly one
of api_url or api_key is Some to avoid silent misconfiguration; add
tracing::debug! logs on each branch decision — when selecting the custom
endpoint log the URL host (never the key) and when falling back log that
OpenHuman backend is used; and add a unit test in the tests module that calls
create_backend_inference_provider(Some("https://api.example.com"),
Some("sk-test"), &ProviderRuntimeOptions::default()) and asserts the result is
Ok() to cover the custom api_key path (use OpenAiCompatibleProvider::new and
openhuman_backend::OpenHumanBackendProvider::new as reference points).

---

Outside diff comments:
In `@scratch.js`:
- Around line 1-3: Remove the accidental development scratch file scratch.js
from the PR; delete the file (or if you want to keep it for personal local
experiments, move it out of the repo and add its path to .gitignore) and ensure
any stray import statements like callCoreRpc from
./app/src/services/coreRpcClient.ts are not left in the tree because that
relative TS import is invalid from a JS file at the repo root.

In `@src/openhuman/config/ops.rs`:
- Around line 228-246: The current code saves the real API key into config and
then calls snapshot_config_json(config), which returns the raw secret; instead,
after persisting via config.save() keep the real key in storage but create a
sanitized copy for snapshots: clone the config (or construct a sanitized
Config), clear or replace config.api_key on that copy (e.g., set
sanitized.api_key = None or Some("[REDACTED]")), and pass the sanitized copy to
snapshot_config_json instead of the real config; reference update.api_key,
config.api_key, config.save(), and snapshot_config_json in your changes.

In `@src/openhuman/providers/ops.rs`:
- Around line 331-343: Add a unit test that covers the new OpenAI-compatible
branch by calling create_backend_inference_provider with both Some(api_url) and
Some(api_key) (and a ProviderRuntimeOptions::default()) and asserting the Result
is Ok; additionally, downcast or inspect the returned provider to verify its
identity/type or id matches the OpenAI-compatible provider (reference
create_backend_inference_provider and ProviderRuntimeOptions to locate the
factory and the provider type/id used in the new branch), ensuring the test
lives alongside the existing factory_backend test in the #[cfg(test)] mod.

In `@src/openhuman/routing/factory.rs`:
- Around line 43-74: Run rustfmt/cargo fmt on this hunk to fix formatting around
the matches! and if/else block; specifically reformat the code that computes
use_openai_compat_local and the subsequent let (provider_label, local_base,
health) = ... expression (which references override_base, provider_kind,
local_ai_config, ollama_base, LocalHealthChecker) so it matches cargo fmt style.
Execute `cargo fmt --all` (or rustfmt on src/openhuman/routing/factory.rs) and
recommit the changed file; no logical changes required, only formatting.

In `@src/openhuman/tools/impl/agent/delegate.rs`:
- Around line 187-198: Add an api_key: Option<String> field to DelegateTool and
thread it through its constructors (new_with_options and
with_depth_and_options), ensuring callers set it from the config context in
tools/ops.rs; then replace the hardcoded None in execute() when calling
providers::create_backend_inference_provider with self.api_key.as_deref() so the
provider factory receives the configured API key; ensure any constructor
signatures and the call site in tools/ops.rs are updated to accept and forward
the api_key from the available config.

---

Nitpick comments:
In `@src/openhuman/providers/ops.rs`:
- Around line 156-162: Introduce a sibling public constant for the hardcoded
provider id and use it in the OpenAiCompatibleProvider instantiation: add pub
const CUSTOM_OPENAI_BACKEND_ID: &str = "custom_openai"; near the existing
INFERENCE_BACKEND_ID, then replace the inline "custom_openai" in the call to
OpenAiCompatibleProvider::new with CUSTOM_OPENAI_BACKEND_ID; keep the
AuthStyle::Bearer usage as-is but ensure the new constant is exported so other
modules (metrics, routing, fallbacks) can reference it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc94b6a9-2e47-4b51-835b-35a34af21b69

📥 Commits

Reviewing files that changed from the base of the PR and between d2f0457 and d72af25.

📒 Files selected for processing (18)
  • app/src/components/settings/panels/LocalModelDebugPanel.tsx
  • app/src/components/settings/panels/local-model/CustomModelSection.tsx
  • app/src/utils/tauriCommands/config.ts
  • scratch.js
  • src/openhuman/agent/harness/session/builder.rs
  • src/openhuman/agent/triage/routing.rs
  • src/openhuman/channels/routes.rs
  • src/openhuman/channels/runtime/startup.rs
  • src/openhuman/config/ops.rs
  • src/openhuman/config/schema/local_ai.rs
  • src/openhuman/config/schema/types.rs
  • src/openhuman/config/schemas.rs
  • src/openhuman/learning/linkedin_enrichment.rs
  • src/openhuman/local_ai/ops.rs
  • src/openhuman/providers/ops.rs
  • src/openhuman/routing/factory.rs
  • src/openhuman/threads/ops.rs
  • src/openhuman/tools/impl/agent/delegate.rs

Comment on lines +7 to +112
const CustomModelSection = () => {
const [apiUrl, setApiUrl] = useState('');
const [apiKey, setApiKey] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState('');
const [saveSuccess, setSaveSuccess] = useState(false);

useEffect(() => {
let mounted = true;
const fetchConfig = async () => {
try {
const { result } = await openhumanGetConfig();
if (mounted) {
setApiUrl(String(result.config.api_url || ''));
setApiKey(String(result.config.api_key || ''));
}
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err.message : 'Failed to load custom backend config');
}
} finally {
if (mounted) setIsLoading(false);
}
};
void fetchConfig();
return () => {
mounted = false;
};
}, []);

const handleSave = async () => {
setIsSaving(true);
setError('');
setSaveSuccess(false);
try {
await openhumanUpdateModelSettings({
api_url: apiUrl.trim() || null,
api_key: apiKey.trim() || null,
});
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save custom backend settings');
} finally {
setIsSaving(false);
}
};

return (
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Custom Provider (OpenAI Compatible)</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-4">
<p className="text-xs text-stone-500">
Configure a custom OpenAI-compatible backend (like vLLM or LiteLLM). If set, this will route requests to your custom endpoint instead of the standard local or built-in backends.
</p>

{isLoading ? (
<div className="text-sm text-stone-500 animate-pulse">Loading settings…</div>
) : (
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-stone-700 mb-1">
Base URL
</label>
<input
type="text"
value={apiUrl}
onChange={e => setApiUrl(e.target.value)}
placeholder="http://localhost:8000/v1"
className="w-full rounded-md border border-stone-300 bg-white px-3 py-2 text-sm text-stone-900 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-stone-700 mb-1">
API Key
</label>
<input
type="password"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
placeholder="sk-..."
className="w-full rounded-md border border-stone-300 bg-white px-3 py-2 text-sm text-stone-900 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
</div>

<div className="flex items-center gap-3 pt-2">
<button
type="button"
onClick={handleSave}
disabled={isSaving}
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 focus:outline-none disabled:opacity-50">
{isSaving ? 'Saving…' : 'Save Config'}
</button>
{saveSuccess && <span className="text-xs text-green-600">Saved successfully.</span>}
</div>

{error && <div className="text-xs text-red-600 mt-2">{error}</div>}
</div>
)}
</div>
</section>
);
};

export default CustomModelSection;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Please add a colocated Vitest for this new settings flow.

This component adds non-trivial behavior around initial load, blank-to-null normalization, save failures, and the transient success state, but there isn’t a CustomModelSection.test.tsx covering those paths in the changes here. This feature is now part of credential/config handling, so it’s worth pinning down before it ships.

As per coding guidelines, "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top" and "app/src/**/*.{ts,tsx}: Co-locate unit tests as *.test.ts or *.test.tsx under app/src/**; use Vitest with app/test/vitest.config.ts."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/local-model/CustomModelSection.tsx` around
lines 7 - 112, Add a colocated Vitest file CustomModelSection.test.tsx that
imports CustomModelSection and tests its key behaviors: mock openhumanGetConfig
to return initial config and assert that apiUrl/apiKey are populated on mount
and loading state toggles; mock openhumanGetConfig to throw and assert error
display; test handleSave by mocking openhumanUpdateModelSettings and asserting
that blank inputs are sent as null (normalization), that isSaving disables the
button and that the transient saveSuccess message appears then disappears (use
fake timers to advance 3s); also test save failure by having
openhumanUpdateModelSettings reject and asserting the error message is shown;
use vi.mock to stub openhumanGetConfig/openhumanUpdateModelSettings and
`@testing-library/react` for render/userEvent queries.

Comment on lines 36 to +38
import ModelDownloadSection from './local-model/ModelDownloadSection';
import ModelStatusSection from './local-model/ModelStatusSection';
import CustomModelSection from './local-model/CustomModelSection';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prettier check is failing on this file.

Pipeline reports Prettier --check style issues. The new CustomModelSection import is out of alphabetical order relative to the adjacent ./local-model/* imports and there's a stray blank line before the new element at L447–448. Run pnpm prettier --write on this file before merging.

🎨 Likely fix
-import ModelDownloadSection from './local-model/ModelDownloadSection';
-import ModelStatusSection from './local-model/ModelStatusSection';
-import CustomModelSection from './local-model/CustomModelSection';
+import CustomModelSection from './local-model/CustomModelSection';
+import ModelDownloadSection from './local-model/ModelDownloadSection';
+import ModelStatusSection from './local-model/ModelStatusSection';
         />
-
-
-        <CustomModelSection />
+
+        <CustomModelSection />

As per coding guidelines: "Run Prettier and ESLint pre-merge; use tsc --noEmit in app/ to check for type errors".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/LocalModelDebugPanel.tsx` around lines 36
- 38, Imports from ./local-model are not alphabetized and there's an extra blank
line in the JSX which causes Prettier to fail; reorder the imports so
ModelDownloadSection, ModelStatusSection, CustomModelSection are alphabetically
sorted (place CustomModelSection in the correct spot), remove the stray blank
line before the new JSX element near the bottom of LocalModelDebugPanel.tsx,
then run pnpm prettier --write on the file (and optionally tsc --noEmit in app/)
to ensure style/type checks pass.

Comment on lines +14 to +15
#[serde(default)]
pub api_key: Option<String>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find any Debug/tracing prints of LocalAiConfig or config.local_ai that could exfiltrate the new api_key.
rg -nP --type=rust -C2 '\blocal_ai\b.*\{:\?\}|\?.*local_ai|tracing::\w+!\([^)]*local_ai'
rg -nP --type=rust -C2 '\{:\?\}.*LocalAiConfig|LocalAiConfig.*\{:\?\}'

Repository: tinyhumansai/openhuman

Length of output: 8781


🏁 Script executed:

# Find the LocalAiConfig struct definition and its derives
rg -nA 20 'struct LocalAiConfig' src/openhuman/config/schema/local_ai.rs

Repository: tinyhumansai/openhuman

Length of output: 888


🏁 Script executed:

# Check if there's a custom Debug impl for LocalAiConfig
rg -nA 10 'impl.*Debug.*LocalAiConfig|impl.*LocalAiConfig.*Debug' src/openhuman/config/schema/

Repository: tinyhumansai/openhuman

Length of output: 48


🏁 Script executed:

# More targeted search for {:?} debug prints of the entire config.local_ai
rg -nP --type=rust 'tracing::\w+!\([^)]*\{:\?\}[^)]*local_ai|local_ai[^)]*\{:\?\}'

Repository: tinyhumansai/openhuman

Length of output: 259


🏁 Script executed:

# Get the full struct definition including derives
rg -nB 5 'pub struct LocalAiConfig' src/openhuman/config/schema/local_ai.rs | head -20

Repository: tinyhumansai/openhuman

Length of output: 230


🏁 Script executed:

# Check what service_state is at that line in ops.rs
rg -nB 10 -A 5 'service_state=\{:\?\}' src/openhuman/app_state/ops.rs

Repository: tinyhumansai/openhuman

Length of output: 828


🏁 Script executed:

# Search for any debug macro invocations that might dump config.local_ai or LocalAiConfig with {:?}
rg -nP --type=rust 'debug!\([^)]*\?[^)]*local_ai|tracing::debug!\([^)]*\?[^)]*local_ai'

Repository: tinyhumansai/openhuman

Length of output: 328


🏁 Script executed:

# Also check if LocalAiConfig is ever logged anywhere with the ? shorthand
rg -nP --type=rust '\?\s*config\.local_ai|\?\s*local_ai\b'

Repository: tinyhumansai/openhuman

Length of output: 174


🏁 Script executed:

# Check if there are any existing redacting newtypes or Secret wrappers in the codebase
rg -nA 5 'struct Secret|newtype.*secret|RedactedString' src/

Repository: tinyhumansai/openhuman

Length of output: 835


Implement a custom Debug impl or skip the Debug derive for LocalAiConfig to redact the api_key field.

LocalAiConfig derives Debug and contains pub api_key: Option<String>. While no current debug prints of the full struct exist, the derived Debug impl will expose the raw API key if ever used in tracing::debug!(?config.local_ai) or similar debug output. Per coding guidelines: "Never log secrets, raw JWTs, API keys, credentials, or full PII; redact or omit sensitive fields."

Add a custom Debug impl that prints api_key: "***", or remove Debug from the derive macro if it's not needed elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/config/schema/local_ai.rs` around lines 14 - 15, LocalAiConfig
currently derives Debug and exposes the sensitive api_key field; implement a
custom Debug impl for LocalAiConfig (or remove Debug from the derive) so that
api_key is redacted (e.g., always print api_key: "***") instead of its raw
value; update the impl to reference the struct name LocalAiConfig and the
api_key field so any debug printing (for example via
tracing::debug!(?config.local_ai)) will not leak the API key.

Comment on lines 150 to 168
/// Create the OpenHuman backend inference client (session JWT only).
pub fn create_backend_inference_provider(
api_url: Option<&str>,
api_key: Option<&str>,
options: &ProviderRuntimeOptions,
) -> anyhow::Result<Box<dyn Provider>> {
Ok(Box::new(openhuman_backend::OpenHumanBackendProvider::new(
api_url, options,
)))
if let (Some(url), Some(key)) = (api_url, api_key) {
Ok(Box::new(crate::openhuman::providers::compatible::OpenAiCompatibleProvider::new(
"custom_openai",
url,
Some(key),
crate::openhuman::providers::compatible::AuthStyle::Bearer,
)))
} else {
Ok(Box::new(openhuman_backend::OpenHumanBackendProvider::new(
api_url, options,
)))
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect OpenAiCompatibleProvider::new signature to confirm whether
# ProviderRuntimeOptions fields (reasoning_enabled, auth_profile_override,
# secrets_encrypt, openhuman_dir) have a counterpart in the compatible provider.
fd -e rs compatible
ast-grep --pattern $'impl OpenAiCompatibleProvider {
  $$$
  pub fn new($$$) -> $_ {
    $$$
  }
  $$$
}'
ast-grep --pattern $'pub enum AuthStyle {
  $$$
}'

Repository: tinyhumansai/openhuman

Length of output: 17603


🏁 Script executed:

#!/bin/bash
# Locate ops.rs and inspect lines 145-180 to verify the code structure and logging
fd -type f -name "ops.rs" | head -5

Repository: tinyhumansai/openhuman

Length of output: 237


🏁 Script executed:

#!/bin/bash
# Read ops.rs to confirm lines 150-168 match the review's snippet
if fd -type f -name "ops.rs" | grep -q "src/openhuman/providers/ops.rs"; then
  echo "=== ops.rs lines 145-180 ===" 
  sed -n '145,180p' src/openhuman/providers/ops.rs
  echo ""
  echo "=== ops.rs lines 330-350 (test area) ===" 
  sed -n '330,350p' src/openhuman/providers/ops.rs
fi

Repository: tinyhumansai/openhuman

Length of output: 237


🏁 Script executed:

#!/bin/bash
# Check file size of ops.rs
wc -l src/openhuman/providers/ops.rs

Repository: tinyhumansai/openhuman

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Correct: read ops.rs lines 145-180
sed -n '145,180p' src/openhuman/providers/ops.rs

Repository: tinyhumansai/openhuman

Length of output: 1439


🏁 Script executed:

#!/bin/bash
# Check test area lines 330-350
sed -n '330,350p' src/openhuman/providers/ops.rs

Repository: tinyhumansai/openhuman

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Check if there are any tracing/logging statements in the file
rg "tracing::" src/openhuman/providers/ops.rs

Repository: tinyhumansai/openhuman

Length of output: 90


Update rustdoc, add branch-decision logging, handle unused options parameter explicitly, and provide test coverage for the custom API endpoint path.

This newly-branched factory has several gaps:

  1. Stale rustdoc (line 150) — The comment still says "session JWT only", but the function can now return OpenAiCompatibleProvider. Update to document that api_key enables Bearer-authenticated custom OpenAI-compatible endpoints.

  2. options: &ProviderRuntimeOptions is silently ignored on the custom path (lines 156–162). Settings like reasoning_enabled and auth_profile_override have no effect when a custom endpoint is configured. Either add a comment acknowledging this or thread relevant fields into OpenAiCompatibleProvider::new() if that becomes a requirement.

  3. api_key is silently discarded when api_url is None (and vice versa). A user who provides a key without the URL will get the default OpenHuman backend with no warning. Add a tracing::warn! when exactly one of the pair is provided, to prevent silent misconfiguration.

  4. Missing branch-decision logging. Per coding guidelines, branch decisions require development-oriented logs with grep-friendly prefixes. Add tracing::debug! when selecting the custom endpoint (recording the URL host, never the key) and when falling back to OpenHuman backend.

  5. No test coverage for the new custom api_key path. The only test (lines 335–343) covers (None, None). Add a test for the (Some(url), Some(key)) branch.

🧩 Example shape
-/// Create the OpenHuman backend inference client (session JWT only).
+/// Create an inference backend provider.
+///
+/// * If both `api_url` and `api_key` are provided, returns an
+///   [`OpenAiCompatibleProvider`] with Bearer authentication.
+/// * Otherwise, returns an [`OpenHumanBackendProvider`]. Note: `ProviderRuntimeOptions`
+///   only applies to the OpenHuman backend path.
 pub fn create_backend_inference_provider(
     api_url: Option<&str>,
     api_key: Option<&str>,
     options: &ProviderRuntimeOptions,
 ) -> anyhow::Result<Box<dyn Provider>> {
+    if api_key.is_some() != api_url.is_some() {
+        tracing::warn!(
+            target: "providers",
+            "[providers] api_key/api_url mismatch; ignoring partial config and using OpenHuman backend"
+        );
+    }
     if let (Some(url), Some(key)) = (api_url, api_key) {
+        tracing::debug!(
+            target: "providers",
+            provider = "custom_openai",
+            "[providers] selecting custom OpenAI-compatible backend"
+        );
         Ok(Box::new(crate::openhuman::providers::compatible::OpenAiCompatibleProvider::new(
             "custom_openai",
             url,
             Some(key),
             crate::openhuman::providers::compatible::AuthStyle::Bearer,
         )))
     } else {
+        tracing::debug!(
+            target: "providers",
+            "[providers] selecting OpenHuman backend"
+        );
         Ok(Box::new(openhuman_backend::OpenHumanBackendProvider::new(
             api_url, options,
         )))
     }
 }

For the test, add to the tests module:

#[test]
fn factory_backend_custom_api() {
    let result = create_backend_inference_provider(
        Some("https://api.example.com"),
        Some("sk-test"),
        &ProviderRuntimeOptions::default(),
    );
    assert!(result.is_ok());
}
🧰 Tools
🪛 GitHub Actions: Type Check

[error] 154-154: cargo fmt --all -- --check failed due to formatting differences (OpenAiCompatibleProvider::new call indentation/line wrapping).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/providers/ops.rs` around lines 150 - 168, Update the
create_backend_inference_provider doc comment to reflect that providing api_key
+ api_url enables a Bearer-authenticated OpenAI-compatible custom endpoint;
ensure ProviderRuntimeOptions is not silently ignored on the custom branch by
either documenting that relevant fields (e.g. reasoning_enabled,
auth_profile_override) are not used or by threading required fields into
OpenAiCompatibleProvider::new; add a tracing::warn! when exactly one of api_url
or api_key is Some to avoid silent misconfiguration; add tracing::debug! logs on
each branch decision — when selecting the custom endpoint log the URL host
(never the key) and when falling back log that OpenHuman backend is used; and
add a unit test in the tests module that calls
create_backend_inference_provider(Some("https://api.example.com"),
Some("sk-test"), &ProviderRuntimeOptions::default()) and asserts the result is
Ok() to cover the custom api_key path (use OpenAiCompatibleProvider::new and
openhuman_backend::OpenHumanBackendProvider::new as reference points).

Comment on lines +153 to +176
api_key: Option<&str>,
options: &ProviderRuntimeOptions,
) -> anyhow::Result<Box<dyn Provider>> {
Ok(Box::new(openhuman_backend::OpenHumanBackendProvider::new(
api_url, options,
)))
if let (Some(url), Some(key)) = (api_url, api_key) {
Ok(Box::new(crate::openhuman::providers::compatible::OpenAiCompatibleProvider::new(
"custom_openai",
url,
Some(key),
crate::openhuman::providers::compatible::AuthStyle::Bearer,
)))
} else {
Ok(Box::new(openhuman_backend::OpenHumanBackendProvider::new(
api_url, options,
)))
}
}

/// Create provider chain with retry and fallback behavior.
pub fn create_resilient_provider(
api_url: Option<&str>,
api_key: Option<&str>,
reliability: &crate::openhuman::config::ReliabilityConfig,
) -> anyhow::Result<Box<dyn Provider>> {
create_resilient_provider_with_options(api_url, reliability, &ProviderRuntimeOptions::default())
create_resilient_provider_with_options(api_url, api_key, reliability, &ProviderRuntimeOptions::default())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Run cargo fmt — CI is red on this file.

The Type Check job fails on cargo fmt --all -- --check for lines 154, 173, and 334, which maps to the OpenAiCompatibleProvider::new call wrapping (Lines 157–162), the create_resilient_provider_with_options call on Line 176, and the test-assertion wrapping (Lines 337–342). Please run cargo fmt at both Cargo.toml and app/src-tauri/Cargo.toml before pushing.

As per coding guidelines: "Rust code must follow cargo fmt formatting standards before merging. Run cargo fmt on all changed Rust files."

Also applies to: 337-340

🧰 Tools
🪛 GitHub Actions: Type Check

[error] 154-154: cargo fmt --all -- --check failed due to formatting differences (OpenAiCompatibleProvider::new call indentation/line wrapping).


[error] 173-173: cargo fmt --all -- --check failed due to formatting differences (create_resilient_provider_with_options call line wrapping).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/providers/ops.rs` around lines 153 - 176, Formatting in
create_resilient_provider and surrounding calls (notably the
OpenAiCompatibleProvider::new call and the
create_resilient_provider_with_options invocation) is off and failing CI; run
cargo fmt across the workspace (including all changed Rust files and workspace
Cargo.toml manifests) and reformat the code so create_resilient_provider,
create_resilient_provider_with_options, OpenAiCompatibleProvider::new, and
ProviderRuntimeOptions::default usages follow rustfmt styling, then re-run the
formatter check before pushing.

@unn-Known1 unn-Known1 closed this Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant