Skip to content

Harden Rust custom provider routing and mock coverage#1958

Merged
senamakel merged 6 commits into
tinyhumansai:mainfrom
senamakel:test/custom-provider-harness
May 16, 2026
Merged

Harden Rust custom provider routing and mock coverage#1958
senamakel merged 6 commits into
tinyhumansai:mainfrom
senamakel:test/custom-provider-harness

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 16, 2026

Summary

  • harden Rust custom-provider handling for no-key local providers and auth_style = "none" OpenAI-compatible providers
  • add Rust coverage for invalid keys, malformed URLs, routing aliases, and custom provider edge cases
  • expand the mock LLM harness with smarter request-aware routing and direct tests for OpenAI-compatible path/auth behavior
  • fix web chat provider-role routing so custom reasoning providers do not hijack unrelated backend hint:* workloads
  • move provider unit tests into sibling *_test.rs modules to keep implementation files focused without losing private-item coverage

Problem

  • Custom LLM provider flows had several gaps: unauthenticated local or proxy providers could fail as if an API key were required, malformed provider config was under-tested, and web-chat custom routing could leak into unrelated workload hints.
  • The Rust test harness needed stronger mock behavior so custom provider routing, auth handling, and OpenAI-compatible request paths could be validated deterministically.

Solution

  • Fix provider factory auth selection for Ollama and auth_style = "none", and add explicit regression tests for missing-key, invalid URL, and routing alias cases.
  • Extend the mock LLM route harness with dynamic request rules and direct route tests, then use that harness in JSON-RPC E2Es to assert auth headers, route changes, and backend-model isolation.
  • Refactor provider unit tests out of factory.rs and router.rs into child *_test.rs modules via #[path = ...] to preserve access to private helpers while reducing implementation-file noise.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Local verification was blocked, but CI coverage jobs enforce this gate before merge.
  • N/A: Coverage matrix updated — behaviour/test-harness change, no matrix row changes identified
  • N/A: All affected feature IDs from the matrix are listed in the PR description under ## Related because no matrix row changes were required
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md) — Rust/test-harness only, no release-cut desktop surface change
  • N/A: Linked issue closed via Closes #NNN in the ## Related section — no linked issue for this PR

Impact

  • Rust core only for behavior changes; no intended Tauri/React feature changes.
  • Improves correctness and regression coverage for custom OpenAI-compatible providers, local Ollama-style providers, and web-chat provider routing.
  • No migration expected; existing unrelated workspace warnings remain.

Related

  • Closes: N/A: no linked issue
  • Follow-up PR(s)/TODOs:
    • verify diff-cover merged coverage in CI and close any remaining changed-line coverage gaps if the gate flags them

AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: test/custom-provider-harness
  • Commit SHA: 20e57220

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests:
    • node --test scripts/mock-api/routes/__tests__/llm.test.mjs
    • cargo test --manifest-path Cargo.toml resolve_translates_openhuman_tier_aliases_via_route_table -- --nocapture
    • cargo test --manifest-path Cargo.toml cloud_provider_with_auth_none_does_not_require_api_key -- --nocapture
    • cargo test --manifest-path Cargo.toml provider_role_override_routes_hint_workloads -- --nocapture
    • bash scripts/test-rust-with-mock.sh --test json_rpc_e2e
  • Rust fmt/check (if changed):
    • cargo fmt --all
  • Tauri fmt/check (if changed):
    • pre-push pnpm --filter openhuman-app rust:check

Validation Blocked

  • command: cargo llvm-cov / merged diff-cover verification not completed locally
  • error: mixed rustup/Homebrew toolchain setup made local coverage reporting unreliable
  • impact: changed-line coverage gate still needs confirmation from CI

Behavior Changes

  • Intended behavior change: custom providers route correctly by workload, no-key providers no longer fail as missing-key, and mock-backed E2Es cover those cases
  • User-visible effect: custom provider setup is less likely to break chat routing or fail incorrectly on valid no-auth local/proxy configurations

Parity Contract

  • Legacy behavior preserved: default backend routing remains intact for non-custom workloads
  • Guard/fallback/dispatch parity checks: JSON-RPC E2Es assert hint:agentic stays on backend routing while custom reasoning providers apply only where intended

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this PR
  • Resolution (closed/superseded/updated): updated

@senamakel senamakel requested a review from a team May 16, 2026 19:38
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@senamakel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 5 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d5bbf5a-9910-40fa-a2ac-b4475be89d73

📥 Commits

Reviewing files that changed from the base of the PR and between 20e5722 and 2ee0013.

📒 Files selected for processing (2)
  • scripts/mock-api/routes/__tests__/llm.test.mjs
  • scripts/mock-api/routes/llm.mjs
📝 Walkthrough

Walkthrough

This PR adds rule-based request matching to the mock LLM handler, refactors provider selection to use dynamic role routing based on model hints, updates the session cache fingerprint to track provider bindings instead of fixed reasoning providers, restructures provider factory and router tests into separate files with improved coverage, and extends e2e tests to validate custom reasoning provider routing with authentication.

Changes

Provider Routing and Mock LLM Rule System

Layer / File(s) Summary
Mock LLM request rule matching and streaming
scripts/mock-api/routes/llm.mjs, scripts/mock-api/routes/__tests__/llm.test.mjs
Mock LLM handler gains rule-based request matching (URL, model, stream, auth headers, keyword) and conditional error/content responses. Streaming paths use matched rule scripts and fallback content. Tests validate rule matching, streaming/non-streaming paths, and URL variants.
Dynamic provider role selection and cache fingerprint
src/openhuman/agent/harness/session/builder.rs, src/openhuman/channels/providers/web.rs, src/openhuman/channels/providers/web_tests.rs
Provider role selection computed from model hints (hint:agentic, agentic-v1, etc.) instead of hard-coded reasoning. Cache fingerprint shifted from reasoning_provider to provider_binding derived from the selected role. Tests validate role mapping and cache invalidation on binding changes.
Provider factory auth style changes
src/openhuman/providers/factory.rs
Ollama and cloud providers with AuthStyle::None now use CompatAuthStyle::None instead of Bearer when creating OpenAI-compatible providers, aligning auth handling with provider configuration. Tests relocated to separate factory_test.rs file.
Provider factory comprehensive test suite
src/openhuman/providers/factory_test.rs
346 lines of test coverage: provider string parsing (OpenHuman/OpenAI/Anthropic/OpenRouter/Ollama), credential handling, auth style validation, malformed endpoint detection, workload defaulting, and OpenHuman backend fallback behavior.
Provider router comprehensive test suite
src/openhuman/providers/router.rs, src/openhuman/providers/router_test.rs
306 lines of test coverage for RouterProvider: hint-based routing selection, resolve behavior with tier alias translation, warmup, delegation to underlying providers, and tool-call routing with hint support. Tests relocated to separate router_test.rs file.
Credential and doctor validation tests
src/openhuman/credentials/ops_tests.rs, src/openhuman/doctor/core_tests.rs
New test cases for provider credential storage (blank token rejection, api_key extraction) and embedding provider validation (standard providers, custom URLs, scheme validation).
E2E request capture and custom reasoning provider coverage
tests/json_rpc_e2e.rs
Extends JSON-RPC e2e test harness with request capture infrastructure (path, model, headers, auth). Adds two custom reasoning-provider e2e tests: one validating stored credential bearer tokens and cache rebuild on provider change, another validating auth_style=none omits auth headers while completing chat. Improves terminal event handling with read_terminal_web_chat_event.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1892: Both PRs modify the mock LLM SSE/streaming implementation in scripts/mock-api/routes/llm.mjs—specifically the handleLlmCompletions/streaming handler selection and streamed chunk behavior.

Suggested labels

working

Poem

🐰 From rule-matched streams to roles that bind,
The chat router finds the path aligned,
Cache fingerprints track the provider's dance,
Bearer tokens and hints advance—
Custom reasoning takes its rightful stance!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: hardening Rust custom provider routing and expanding test coverage with mock harness improvements.
Docstring Coverage ✅ Passed Docstring coverage is 83.87% 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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 16, 2026
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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/openhuman/providers/factory.rs (1)

261-285: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid credential-store lookup for no-auth provider styles.

Line 261 fetches credentials before auth-style branching, so AuthStyle::None (and AuthStyle::OpenhumanJwt) can fail provider creation if credential storage is unreadable, even though these paths do not require API keys.

💡 Suggested fix
-    let key = lookup_key_for_slug(slug, config)?;
-
     match entry.auth_style {
         AuthStyle::Anthropic => {
+            let key = lookup_key_for_slug(slug, config)?;
             let p =
                 make_openai_compatible_provider(&entry.endpoint, &key, CompatAuthStyle::Anthropic)?;
             Ok((p, effective_model))
         }
         AuthStyle::OpenhumanJwt => {
             // Route to the OpenHuman backend — ignore the entry's endpoint
             // and model; use the backend provider with the configured default.
             log::debug!(
                 "[providers][chat-factory] slug='{}' has auth_style=OpenhumanJwt → routing to openhuman backend",
                 slug
             );
             make_openhuman_backend(config)
         }
         AuthStyle::None => {
             let p = make_openai_compatible_provider(&entry.endpoint, "", CompatAuthStyle::None)?;
             Ok((p, effective_model))
         }
         AuthStyle::Bearer => {
+            let key = lookup_key_for_slug(slug, config)?;
             let p =
                 make_openai_compatible_provider(&entry.endpoint, &key, CompatAuthStyle::Bearer)?;
             Ok((p, effective_model))
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/providers/factory.rs` around lines 261 - 285, The code
currently calls lookup_key_for_slug(slug, config) unconditionally which can fail
for providers that don't need credentials; change it to only call
lookup_key_for_slug inside the branches that require a key (AuthStyle::Anthropic
and AuthStyle::Bearer), and keep AuthStyle::None and AuthStyle::OpenhumanJwt
paths using make_openai_compatible_provider or make_openhuman_backend without
performing a credential lookup; update the match arm implementations around
make_openai_compatible_provider, AuthStyle::Anthropic, AuthStyle::Bearer,
AuthStyle::None and the AuthStyle::OpenhumanJwt routing to ensure
lookup_key_for_slug is invoked lazily only where needed.
🧹 Nitpick comments (2)
src/openhuman/channels/providers/web.rs (1)

1335-1342: ⚡ Quick win

Share this provider-role mapping with AgentBuilder.

This table is duplicated here and in src/openhuman/agent/harness/session/builder.rs. If one side gains a new hint/alias and the other doesn't, cache fingerprinting and actual provider construction will drift, which can cause stale session reuse or unnecessary rebuilds. Please move the mapping into one shared helper and reuse it from both call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/channels/providers/web.rs` around lines 1335 - 1342, The
provider_role_for_model_override function duplicates a mapping used by
AgentBuilder; refactor by extracting this mapping into a single shared helper
(e.g., pub fn provider_role_for_model_override or a const MAP in a common
module) and have both provider_role_for_model_override and the AgentBuilder call
site use that shared helper instead of duplicating the match; update references
in provider_role_for_model_override and in the AgentBuilder code in builder.rs
to call the new shared helper so both use the exact same hint/alias-to-role
mapping.
tests/json_rpc_e2e.rs (1)

190-285: 💤 Low value

Consider extracting request capture logic to reduce duplication.

Both chat_completions and generic_chat_completions have nearly identical request-capture blocks (lines 198-216 and 255-273). A helper function could reduce this duplication:

♻️ Suggested refactor
+fn capture_chat_request(uri: &Uri, headers: &HeaderMap, body: &Value) {
+    let auth_header = headers
+        .get(AUTHORIZATION)
+        .and_then(|value| value.to_str().ok())
+        .map(str::to_string);
+    let x_api_key = headers
+        .get("x-api-key")
+        .and_then(|value| value.to_str().ok())
+        .map(str::to_string);
+    with_chat_completion_requests(|requests| {
+        requests.push(json!({
+            "path": uri.path(),
+            "model": body.get("model").and_then(Value::as_str),
+            "stream": body.get("stream").and_then(Value::as_bool),
+            "thread_id": body.get("thread_id").and_then(Value::as_str),
+            "authorization": auth_header,
+            "x_api_key": x_api_key,
+            "body": body.clone(),
+        }))
+    });
+}

 async fn chat_completions(
     uri: Uri,
     headers: HeaderMap,
     Json(body): Json<Value>,
 ) -> Json<Value> {
     if let Some(model) = body.get("model").and_then(Value::as_str) {
         with_chat_completion_models(|models| models.push(model.to_string()));
     }
-    let auth_header = headers
-        .get(AUTHORIZATION)
-        .and_then(|value| value.to_str().ok())
-        .map(str::to_string);
-    // ... (remove duplicate capture block)
+    capture_chat_request(&uri, &headers, &body);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/json_rpc_e2e.rs` around lines 190 - 285, Both chat_completions and
generic_chat_completions duplicate the request-capture block; extract that logic
into a helper (e.g., capture_chat_completion_request or push_chat_request) that
accepts the Uri, HeaderMap and &Value body, builds the auth_header/x_api_key the
same way, constructs the JSON object and calls with_chat_completion_requests to
push it; replace the duplicated blocks in chat_completions and
generic_chat_completions with a single call to that helper to remove duplication
and keep behavior identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/mock-api/routes/llm.mjs`:
- Around line 518-533: The current branch treats any requestRule with status >=
400 as a streaming success by calling writeSseHead() when parsedBody?.stream ===
true; change the logic in the block that checks requestRule?.error ||
(requestRule?.status && requestRule.status >= 400) so that HTTP error status
codes always invoke sendRuleError(res, requestRule) (and return true) instead of
starting an SSE response — only call writeSseHead() / writeSseEvent() when
requestRule indicates a streaming error body (e.g., requestRule.error &&
parsedBody?.stream === true) or when status is < 400; update the conditional
around writeSseHead/writeSseEvent to explicitly require requestRule.status to be
absent or < 400 before starting the SSE stream.

---

Outside diff comments:
In `@src/openhuman/providers/factory.rs`:
- Around line 261-285: The code currently calls lookup_key_for_slug(slug,
config) unconditionally which can fail for providers that don't need
credentials; change it to only call lookup_key_for_slug inside the branches that
require a key (AuthStyle::Anthropic and AuthStyle::Bearer), and keep
AuthStyle::None and AuthStyle::OpenhumanJwt paths using
make_openai_compatible_provider or make_openhuman_backend without performing a
credential lookup; update the match arm implementations around
make_openai_compatible_provider, AuthStyle::Anthropic, AuthStyle::Bearer,
AuthStyle::None and the AuthStyle::OpenhumanJwt routing to ensure
lookup_key_for_slug is invoked lazily only where needed.

---

Nitpick comments:
In `@src/openhuman/channels/providers/web.rs`:
- Around line 1335-1342: The provider_role_for_model_override function
duplicates a mapping used by AgentBuilder; refactor by extracting this mapping
into a single shared helper (e.g., pub fn provider_role_for_model_override or a
const MAP in a common module) and have both provider_role_for_model_override and
the AgentBuilder call site use that shared helper instead of duplicating the
match; update references in provider_role_for_model_override and in the
AgentBuilder code in builder.rs to call the new shared helper so both use the
exact same hint/alias-to-role mapping.

In `@tests/json_rpc_e2e.rs`:
- Around line 190-285: Both chat_completions and generic_chat_completions
duplicate the request-capture block; extract that logic into a helper (e.g.,
capture_chat_completion_request or push_chat_request) that accepts the Uri,
HeaderMap and &Value body, builds the auth_header/x_api_key the same way,
constructs the JSON object and calls with_chat_completion_requests to push it;
replace the duplicated blocks in chat_completions and generic_chat_completions
with a single call to that helper to remove duplication and keep behavior
identical.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4bc9c109-3908-4713-8b1e-a7874172cc5d

📥 Commits

Reviewing files that changed from the base of the PR and between 36a0e73 and 20e5722.

📒 Files selected for processing (12)
  • scripts/mock-api/routes/__tests__/llm.test.mjs
  • scripts/mock-api/routes/llm.mjs
  • src/openhuman/agent/harness/session/builder.rs
  • src/openhuman/channels/providers/web.rs
  • src/openhuman/channels/providers/web_tests.rs
  • src/openhuman/credentials/ops_tests.rs
  • src/openhuman/doctor/core_tests.rs
  • src/openhuman/providers/factory.rs
  • src/openhuman/providers/factory_test.rs
  • src/openhuman/providers/router.rs
  • src/openhuman/providers/router_test.rs
  • tests/json_rpc_e2e.rs

Comment thread scripts/mock-api/routes/llm.mjs
@senamakel senamakel merged commit 30a4a07 into tinyhumansai:main May 16, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant