Skip to content

fix(integrations): fall back to default backend when api_url points at local AI (#51, #80, #7Z)#1630

Merged
senamakel merged 6 commits into
tinyhumansai:mainfrom
oxoxDev:fix/ollama-url-leak
May 14, 2026
Merged

fix(integrations): fall back to default backend when api_url points at local AI (#51, #80, #7Z)#1630
senamakel merged 6 commits into
tinyhumansai:mainfrom
oxoxDev:fix/ollama-url-leak

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 13, 2026

Summary

  • Users who set config.api_url to a local-AI endpoint (Ollama :11434/v1, vLLM :8080/...) had every backend integration request concatenated onto that URL, flooding Sentry with 404s from Ollama/vLLM and silently breaking Composio for them.
  • Guard at IntegrationClient::build_client: if the override looks like a local-AI endpoint (loopback host or /v1/chat/completions path), fall through to env/default for the integrations base. Chat completions still use the override.

Problem

effective_api_url returns whatever the user set in config.api_url. Local-AI users set it to their Ollama / vLLM URL so chat completions hit the local model. But IntegrationClient::build_client reused the same URL as the base for ALL backend-proxied paths (composio, channels, teams, billing, …). Result: requests like /agent-integrations/composio/toolkits got concatenated onto the local-AI URL → 404 from the local LLM → report_error to Sentry → flood, plus silent UX breakage where Composio just didn't work for these users.

Sentry IDs in scope (~13 events total in 3d):

  • OPENHUMAN-TAURI-51 — 11 events: GET http://127.0.0.1:11434/v1/agent-integrations/composio/toolkits 404
  • OPENHUMAN-TAURI-80 — 1 event: GET http://127.0.0.1:8080/v1/chat/completions/agent-integrations/composio/connections 404 (double-concat onto vLLM)
  • OPENHUMAN-TAURI-7Z — 1 event: same shape via llm_provider.api_error: OpenHuman API error (404 Not Found) with the Ollama 404 page body

Solution

  • New looks_like_local_ai_endpoint(url) -> bool in src/api/config.rs. Tight heuristic: loopback host (127.0.0.1 / localhost / ::1 / 0.0.0.0) OR path explicitly names the OpenAI chat-completions endpoint (/v1/chat/completions / /v1/completions). Bare /v1 is not matched — many self-hosted backends use it as a legit version prefix and over-matching would silently break real users.
  • New effective_integrations_api_url(api_url) -> String — same resolution chain as effective_api_url, but the user override is skipped when it matches looks_like_local_ai_endpoint, falling through to env / default backend. One-shot warn! (via std::sync::Once) when the fallback fires so users see the diagnostic in core logs.
  • IntegrationClient::build_client now uses the new helper. effective_api_url semantics are unchanged — local-AI chat needs the override to keep working.

Considered (and deferred): splitting config.api_url into a separate backend_api_url field. That's the "correct" architectural fix but it's a schema migration touching the config UI, dotfile reader, and migration story. The current guard is narrow, reversible, and ships the user-facing fix today; the split can come later if anyone wants explicit control of the integrations base independently.

Submission Checklist

  • Unit tests added for looks_like_local_ai_endpoint (loopback v4/v6, chat-completions path on non-loopback, real backends incl. api.openai.com/v1, garbage input)
  • Unit tests added for effective_integrations_api_url (local override → default, local override + env → env, real override respected, no-override agrees with effective_api_url)
  • cargo test --lib openhuman::integrations passes (67/67)
  • cargo test --lib api passes (124/124, includes new helper tests)
  • cargo fmt --check clean
  • N/A: no UI change in this PR
  • N/A: no schema migration (deliberately narrow; separate backend_api_url field deferred — see Solution)

cargo clippy --workspace -- -D warnings is currently failing on upstream/main itself (78 errors in src/openhuman/wallet/execution.rs, webview_apis/client.rs, webview_notifications/types.rs, tools/impl/cron/add.rs — all manual_is_multiple_of / type_complexity / derivable_impls from a clippy version bump). None of the errors touch the files in this PR. Confirmed via grep -E "api/config\.rs|integrations/client\.rs" over the clippy output — zero hits.

Impact

  • Stops the OPENHUMAN-TAURI-51 / -80 / -7Z Sentry cluster (~13 events / 3d) at the source.
  • Restores Composio + channels + teams functionality for users who route chat to a local LLM — they were silently broken before.
  • Zero change to chat-completions routing: effective_api_url is untouched.

Related

  • Closes OPENHUMAN-TAURI-51, OPENHUMAN-TAURI-80, OPENHUMAN-TAURI-7Z

Summary by CodeRabbit

  • New Features
    • Detects and classifies local AI endpoints (loopback/localhost/0.0.0.0/IPv6, private LAN IPs, and OpenAI-style chat paths) to handle them differently.
  • Bug Fixes
    • Integration client routing now avoids sending integration paths to local AI services, preventing 404s and noisy error reports.
  • Tests
    • Added unit tests for endpoint detection heuristics and integrations URL resolution.

Review Change Stack

oxoxDev and others added 2 commits May 13, 2026 17:02
…solver

Introduce two helpers used by the integrations client to stop routing
backend-proxied requests at a user-set local-AI endpoint:

  * `looks_like_local_ai_endpoint(url)` — tight heuristic. True when the
    host is a loopback name (127.0.0.1 / localhost / ::1 / 0.0.0.0) or
    when the path explicitly names the OpenAI-style chat-completions
    endpoint (/v1/chat/completions or /v1/completions). Bare /v1 is NOT
    matched on purpose — many real self-hosted backends use it as a
    version prefix.
  * `effective_integrations_api_url(api_url)` — same resolution chain as
    `effective_api_url` but skips the user override when it looks like a
    local-AI URL, falling through to env / default backend. Emits one
    `warn!` per process via `std::sync::Once` so the diagnostic shows up
    in core logs without spamming on every request.

Why: `config.api_url` doubles as the chat-completions base AND the
backend base. Users pointing it at Ollama / vLLM had every
`/agent-integrations/composio/*` request 404 against their local LLM,
flooding Sentry (cluster OPENHUMAN-TAURI-51 / -80 / -7Z) and silently
breaking Composio for them. The integrations resolver lets chat keep
the override while integrations fall back to the hosted backend.

Tests cover positive/negative classification (loopback hosts incl. IPv6,
chat-completions paths, real backends, OpenAI public API, garbage input)
and fallback behaviour (local override skipped, env wins when set,
real backends respected, agrees with `effective_api_url` when there is
no override).

Refs: OPENHUMAN-TAURI-51, OPENHUMAN-TAURI-80, OPENHUMAN-TAURI-7Z

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inyhumansai#51, tinyhumansai#80, #7Z)

Switch `IntegrationClient::build_client` from `effective_api_url` to
`effective_integrations_api_url` so users who set `config.api_url` to a
local Ollama / vLLM endpoint don't have every `/agent-integrations/*`
request concatenated onto that local URL (which only knows about
chat-completions and 404s every other path).

Sentry impact:
  * OPENHUMAN-TAURI-51 — 11 events: `GET http://127.0.0.1:11434/v1/
    agent-integrations/composio/toolkits 404`
  * OPENHUMAN-TAURI-80 — 1 event:  `GET http://127.0.0.1:8080/v1/
    chat/completions/agent-integrations/composio/connections 404`
  * OPENHUMAN-TAURI-7Z — 1 event: same shape via
    `llm_provider.api_error: OpenHuman API error (404 Not Found)`
    with the Ollama 404 body.

Beyond the Sentry flood this also restores actual Composio / channels
/ teams functionality for any user pointing chat at a local LLM —
they were silently broken before this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oxoxDev oxoxDev requested a review from a team May 13, 2026 11:33
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7190eaa0-e9f8-41b7-a8d1-dfd089f9c0d2

📥 Commits

Reviewing files that changed from the base of the PR and between b6457e0 and 0f60df3.

📒 Files selected for processing (1)
  • src/api/config.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api/config.rs

📝 Walkthrough

Walkthrough

Adds a local-AI detection heuristic and an integrations-specific API URL resolver that ignores user overrides pointing at local endpoints (with a one-time warning). Updates the integrations client to use the new resolver and extends unit tests for both heuristic and resolver behaviors.

Changes

Integrations API URL Resolution

Layer / File(s) Summary
Local AI endpoint heuristic
src/api/config.rs
looks_like_local_ai_endpoint identifies loopback hosts (127.0.0.1, localhost, 0.0.0.0, ::1), RFC1918 private IPv4 ranges, and OpenAI-style /v1/chat/completions or /v1/completions suffix paths as local; tests cover acceptance, rejection, and malformed/relative inputs.
Integrations URL resolver
src/api/config.rs
effective_integrations_api_url ignores a user api_url when it matches the local-AI heuristic and falls back to BACKEND_URL/VITE_BACKEND_URL or the environment-aware default; includes a one-shot warn helper and tests validating fallback and preservation cases.
Client builder integration
src/openhuman/integrations/client.rs
build_client now uses effective_integrations_api_url(&config.api_url) instead of effective_api_url, preventing integration request paths from being appended to local-AI endpoints.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Suggested reviewers

  • graycyrus

Poem

🐰 I nibble code in moonlit light,
Local endpoints now in sight,
Integrations find the true backend way,
No stray paths to lead astray,
Hooray — the routing’s clear today!

🚥 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 clearly and concisely identifies the main fix: preventing integration requests from being routed to local AI endpoints. It directly addresses the core problem solved by the PR.
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.


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

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Review — PR #1630

Walkthrough

This PR fixes a real and actively-reported user pain point: anyone who sets config.api_url to a local Ollama or vLLM endpoint for chat completions had every backend-proxied integration request (Composio toolkits, channels, billing, teams, credentials, ...) concatenated onto that URL, producing 404s from the local LLM and a Sentry flood. The fix introduces looks_like_local_ai_endpoint, a carefully-scoped heuristic that detects loopback hosts and explicit chat-completions paths, then effective_integrations_api_url, which skips the user override when it looks like a local AI endpoint and falls through to env/default instead. The implementation is clean, well-documented, and comes with solid test coverage.

The core mechanic is correct and the Sentry cluster will stop firing for IntegrationClient callers once this ships.

Changes

File Summary
src/api/config.rs New looks_like_local_ai_endpoint heuristic, effective_integrations_api_url resolver, warn_integrations_url_fallback_once one-shot logger, and 8 unit tests
src/openhuman/integrations/client.rs build_client switched from effective_api_url to effective_integrations_api_url

Actionable comments

[major] src/api/config.rs:74-75contains vs ends_with inconsistency in path check

The check for /v1/chat/completions uses path.contains(...) but /v1/completions uses path.ends_with(...). A URL like https://real-backend.example/audit/v1/chat/completions-logs would match contains and be misclassified. Both arms should use ends_with for consistency:

// before
path.contains("/v1/chat/completions") || path.ends_with("/v1/completions")
// after
path.ends_with("/v1/chat/completions") || path.ends_with("/v1/completions")

[major] src/openhuman/integrations/client.rs:241-246 — doc comment still references old resolver

The rustdoc says effective_api_url but the implementation now uses effective_integrations_api_url. Should be updated to match.

[minor] Incomplete fix scope — 38+ other effective_api_url callers remain broken for local-AI users

The same class of bug exists across most non-integrations domains: billing/ops.rs, team/ops.rs, referral/ops.rs, webhooks/ops.rs, credentials/ops.rs (9 call sites), channels/controllers/ops.rs (9 call sites), voice/, socket/, app_state/, core/jsonrpc.rs, etc. The cleanest near-term fix would be to rename effective_integrations_api_url to effective_backend_api_url and make it the default for all non-chat callers. This PR makes things no worse (these were already broken), but a follow-up issue should be filed explicitly tracking these callers.

Nitpick

  • src/api/config.rs:51trimmed is computed but url::Url::parse receives trimmed correctly. However, the url::Url::parse(trimmed) line is fine as-is — ignore if no concern.

Verified / looks good

  • url::Host typed matching handles IPv4-mapped IPv6 and ::1 correctly
  • std::sync::Once one-shot warning prevents log spam
  • No panic on malformed URLs (garbage input test confirms)
  • ENV_LOCK mutex used consistently with existing test suite
  • CI failures are all in composio::ops::tests — pre-existing, unrelated to this PR
  • No secrets or PII in new log output

…nsai#1630 CR)

graycyrus majors:

1. `looks_like_local_ai_endpoint` used `path.contains("/v1/chat/completions")`
   asymmetric with `path.ends_with("/v1/completions")`. A real backend URL
   like `/audit/v1/chat/completions-logs` would have been misclassified
   as local-AI and silently dropped. Both arms now use `ends_with`.

2. `build_client` rustdoc still referenced the old `effective_api_url`
   resolver - updated to `effective_integrations_api_url`.

3. Filed follow-up issue tinyhumansai#1663 tracking the 38+ other `effective_api_url`
   callers across non-integrations domains (billing, team, referral,
   webhooks, credentials, channels, voice). TODO note added in the
   resolver pointing at the issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 13, 2026

graycyrus review Round 1 addressed in 39d60eb3:

  • [Major] looks_like_local_ai_endpoint path check now uses ends_with on both arms; new unit test looks_like_local_ai_rejects_substring_path_false_positives locks the false-positive case (/audit/v1/chat/completions-logs → false, plus a couple of other substring-shaped real-backend paths).
  • [Major] build_client rustdoc updated to reference effective_integrations_api_url and call out why it differs from the plain effective_api_url resolver (local-AI fallback).
  • [Minor / scope] Filed follow-up Migrate all backend-API callers from effective_api_url to effective_backend_api_url (local-AI users) #1663 tracking the 38+ other effective_api_url callers across billing / team / referral / webhooks / credentials / channels / voice / socket / app_state / core/jsonrpc. The TODO note above effective_integrations_api_url references it. Renaming effective_integrations_api_urleffective_backend_api_url and migrating those call sites is a much larger surface, so I'd rather ship it separately under that issue than expand this PR.

Open question: do you want the typed-Url host check kept as-is, or would you prefer a tighter allowlist (e.g. explicit .is_loopback() only without the *.localhost domain match)? Happy to tighten further if so.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
@senamakel senamakel self-assigned this May 13, 2026
senamakel added 2 commits May 13, 2026 11:15
Extends `looks_like_local_ai_endpoint` to include `addr.is_private()`
on the IPv4 match arm so LAN-hosted Ollama (e.g. 192.168.x.x, 10.x.x.x,
172.16.x.x) is correctly classified as local-AI, preventing integration
requests from being routed at the local LLM and 404-ing.

Also clarifies the `warn_integrations_url_fallback_once` doc comment to
explicitly state the Once guard suppresses subsequent calls even when a
different local-AI URL is used.

Test coverage: replaced ambiguous 10.0.0.5 non-loopback test with a
public IP (203.0.113.5) and .example TLD; added new
`looks_like_local_ai_matches_private_lan_hosts` covering all three RFC
1918 ranges. 21/21 lib tests pass.

Addresses @coderabbitai review on src/api/config.rs.
@senamakel
Copy link
Copy Markdown
Member

Deferred items (context for reviewers — no action needed on this PR)

Two minor items were identified during the review pass but are intentionally out of scope:

  1. File sizesrc/api/config.rs is now ~600 lines, slightly over the project's preferred ~500-line ceiling. The overage is test-only (#[cfg(test)] block). Splitting the tests into a sibling src/api/config/tests.rs would be the right long-term move, but is a pure refactor with no behaviour change. Deferred to a follow-up.

  2. EnvGuard test helper — several tests in this file set/unset env vars by hand (std::env::set_var / remove_var) with manual teardown. A small EnvGuard RAII helper would eliminate the boilerplate and make tests panic-safe. Deferred as a refactor-only item.

Neither item affects correctness or CI for this PR.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
Loopback alone is too aggressive a local-AI signal: integration tests
(e.g. composio/ops_tests.rs) bind mock backends on `127.0.0.1:<random
port>` with no path, and were being silently rerouted to the production
backend by `effective_integrations_api_url`, producing 401s in CI.

Tighten the heuristic so a loopback / private RFC 1918 host classifies
as local-AI only when paired with an additional LLM signal — a known
port (11434 Ollama, 8000 vLLM, 8080, 1234, 8888) or a `/v1` path. All
real-world Sentry cases (OPENHUMAN-TAURI-51/-80/-7Z) include one of
these signals, and ad-hoc test mocks include neither.

Promotes the chat-completions path check above the host check so
`/v1/chat/completions` on any host (LAN, tunnel, public IP) still
matches — preserves the existing non-loopback test assertion.

Adds a regression test asserting bare loopback + random port is NOT
classified as local-AI.

cargo test --lib: 6608 pass, 0 fail (including all 39 composio ops
tests that were failing under the previous heuristic).
@senamakel senamakel merged commit 7008389 into tinyhumansai:main May 14, 2026
24 checks passed
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.

3 participants