--- name: omp-search-provider-fallback-debug description: "Diagnose why the omp harness web_search silently uses a fallback provider (e.g. Kagi) instead of the configured primary (SearXNG): the fallbackChains masking pattern, the searxng.endpoint base-URL scheme/guard-chain trap, and the cli.js + content-type probes that prove it." --- # omp web_search silently using the wrong provider Symptom: agent searches return results from a fallback provider (e.g. **Kagi**) even though `modelRoles.web` is set to the intended primary (e.g. `web/searxng`). The primary errors every time and the failure is invisible because a fallback answers successfully. ## The masking mechanism (always check this first) `~/.omp/agent/config.yml`: - `modelRoles.web:` is the primary (e.g. `web/searxng`). - `retry.fallbackChains.web:` is an ordered list tried when the primary throws. The FIRST entry silently serves whenever the primary errors. In this fleet it is `web/kagi`, so any SearXNG error == Kagi answers and you never see an error. So "searches go through Kagi" almost always means "SearXNG is failing", not a routing mistake. ## The SearXNG endpoint trap (the usual root cause here) `searxng.endpoint` in config.yml must be a **full base URL pointing at the direct JSON port**, used **verbatim** by omp (no scheme auto-added, no normalize — confirmed in `cli.js`). Two ways it breaks: 1. **Bare host** (e.g. `search.franksdank.cloud`, no scheme): omp does `new URL(\`${endpoint}/search\`)` → **throws** immediately (URL needs a scheme). SearXNG never contacted. 2. **The guarded vhost** (`https://search.franksdank.cloud`): returns Anubis' **"Making sure you're not a bot!"** challenge page at HTTP **200 `text/html`**. omp calls `o.json()` unconditionally regardless of content-type → throws on the leading `<`. There is deliberately NO API path through the guard chain (JS challenge + UA denylist). **Correct value** (direct published port, http scheme preserved — reachable from the LAN only): ```yaml searxng: endpoint: http://192.168.0.143:8081 # Dankclaw; NOT the vhost, NOT schemeless ``` The published port source (192.168.x/loopback/LAN) is in SearXNG's `pass_ip`, so it bypasses the JSON rate limiter (hardcoded `API_MAX=4`/IP/hr, not settable except via `pass_ip`). ## Prove it (don't guess) 1. Content-type probe, both paths — the decisive check: ``` curl -sk -o /tmp/vh -w "vhost ct=%{content_type}\n" "https://search.franksdank.cloud/search?q=test&format=json" # expect text/html (Anubis) curl -s -o /tmp/dp -w "direct ct=%{content_type}\n" "http://192.168.0.143:8081/search?q=test&format=json" # expect application/json ``` vhost = `text/html` + direct = `application/json` confirms the trap. 2. Code path, in the installed omp dist (NOT a worktree): `/home/blank/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js` (find install: `dirname $(readlink -f $(which omp))`). Grep the SearXNG provider: - `searxng.endpoint` config def → description "Base URL of a self-hosted SearXNG instance". - the request builder `...=e.replace(/\/+$/,""),new URL(\`${r}/search\`)...` → endpoint is verbatim, scheme NOT added. - `X2c`/`J2c` → `if(!o.ok)throw` then `return await o.json()` → HTML 200 passes `.ok` and blows up in `.json()`. ## Apply + verify - Fix is a harness config edit (`cfg://searxng/endpoint` prompts the user, or edit config.yml). Confirm with the user first — it is their live harness config. - Verification after applying: fire a real `web_search` and confirm it returns `provider:"searxng"` (not `"searxng"`-then-fallback / not `"kagi"`). ## Related gotcha (same masking, different cause) If SearXNG answers JSON but returns empty `results` with `unresponsive_engines` (VPN-exit-IP CAPTCHA/429 on google/brave), omp's `vcs` throws a 503 → Kagi again. That's an engine-health problem, fix in `[[SearXNG]]`/`[[SearXNG Providers]]`, not the endpoint. ## Hardening option Drop or reorder `web/kagi` / `web/perplexity` in `retry.fallbackChains.web` so primary failures surface instead of silently spending a paid Kagi credit. Flag to user before editing.