Skip to content

feat: wire interceptFetch into the chat pipeline + dashboard toggle (#7339)#7736

Merged
diegosouzapw merged 2 commits into
release/v3.8.49from
feat/7339-intercept-fetch-wiring
Jul 19, 2026
Merged

feat: wire interceptFetch into the chat pipeline + dashboard toggle (#7339)#7736
diegosouzapw merged 2 commits into
release/v3.8.49from
feat/7339-intercept-fetch-wiring

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

Tracks the deferred Phases 3-4 of #3384 (Phases 1-2 shipped the DB schema + resolveInterceptSearch in release/v3.8.47).

  • resolveInterceptFetch(provider, model) — a structural twin of resolveInterceptSearch (src/lib/db/interceptionRules.ts), same per-model > per-provider > undefined precedence.
  • New open-sse/services/webFetchInterception.ts (mirrors webSearchFallback.ts) — pure request-body transform that rewrites a provider-native web_fetch tool declaration into a synthetic omniroute_web_fetch function tool. No HTTP, no SSE/streaming, no abort-signal handling in this module.
  • The synthetic tool call is routed through the existing handleToolCallExecution dispatch path (same one already proven for omniroute_web_search) to a new web_fetch builtin skill handler (src/lib/skills/builtins.ts + new src/lib/skills/webFetchExecution.ts), which resolves web-fetch provider credentials and calls handleWebFetch() against /v1/web/fetch.
  • Single new call site in open-sse/handlers/chatCore.ts (frozen file, ~5125-line cap) directly beside the existing interceptSearch block: one resolver call + one prepareWebFetchFallbackBody call, plus threading provider/effectiveModel into the existing handleToolCallExecution call so the builtin can resolve a pinned fetch backend.

Owner decision — ships the dashboard toggle in this PR (overriding the plan's backend-only recommendation, since no such toggle exists for the interceptSearch sibling either):

  • ProviderInterceptionSection.tsx — new card on the provider detail page with two toggles (interceptSearch, interceptFetch), since they share one interception-rules DB row per provider. Mounted from the existing ProviderExtraPanels.tsx escape valve (frozen ProviderDetailPageClient.tsx untouched).
  • New GET/PUT/DELETE /api/providers/[id]/interception-rules route, mirroring the existing param-filters route pattern exactly (requireManagementAuth, Zod validation, buildErrorBody/sanitizeErrorMessage).
  • i18n keys added to both en.json and pt-BR.json.
  • Provider-level toggle only in this pass (no per-model UI row yet) — kept minimal; per-model overrides remain configurable via the same API's models field for now.

Hot-path safety / zero overhead when disabled: unlike interceptSearch (which predates #3384 and already ran a native-bypass heuristic in production before the override was added), interceptFetch is brand-new behavior. To guarantee true opt-in and avoid any surprise default conversion, supportsNativeWebFetchFallbackBypass was simplified to interceptFetchOverride !== true — strictly opt-in, no heuristic default. With no interceptFetch DB row configured (the default/common case), the outgoing request body is byte-identical to pre-#7339 behavior.

How validated (Hard Rule #18 — TDD)

Confirmed red-before-green against the base ref: resolveInterceptFetch and open-sse/services/webFetchInterception.ts do not exist on origin/release/v3.8.49 (git show origin/release/v3.8.49:open-sse/services/webFetchInterception.ts → fatal: path does not exist; grep -c resolveInterceptFetch on the base interceptionRules.ts → 0). All new tests pass on this branch (green).

New test files (21 tests total):

  • tests/unit/intercept-fetch-resolver.test.tsresolveInterceptFetch precedence (undefined / provider-level / model-override-wins / field independence from interceptSearch / empty-provider).
  • tests/unit/web-fetch-interception.test.tsprepareWebFetchFallbackBody / supportsNativeWebFetchFallbackBypass (no-op with no tools, no-op with no web_fetch tool, converts when override=true, flat-vs-nested shape by target format, untouched when override=false, preserves sibling tools, opt-in-only bypass semantics).
  • tests/unit/web-fetch-dispatch.test.tshandleToolCallExecution routes omniroute_web_fetch to the web_fetch builtin and returns the OpenAI tool-result envelope; an unaliased tool name is left untouched; an error thrown mid-fetch (simulated AbortError) is surfaced in the result, not silently dropped (Hard Rule fix(oauth): broaden upsert to match any existing connection #6).
  • tests/unit/chat-core-intercept-fetch.test.ts — regression guard mirroring the exact two-call chatCore.ts sequence: with no interceptFetch rule configured, the outgoing body is the same object reference (true no-op); a request with no web_fetch tool is untouched regardless of rule state; the tool is only converted once a provider/model explicitly opts in.

Commands run:

node --import tsx/esm --test tests/unit/intercept-fetch-resolver.test.ts tests/unit/web-fetch-interception.test.ts tests/unit/web-fetch-dispatch.test.ts tests/unit/chat-core-intercept-fetch.test.ts
# 21 pass, 0 fail

Gates run (all green)

  • node scripts/check/check-test-discovery.mjs
  • npm run typecheck:core
  • npm run typecheck:noimplicit:core (3 pre-existing unrelated errors on base, none in touched files)
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>
  • npm run check:complexity-ratchets
  • node scripts/check/check-file-size.mjs (chatCore.ts is well under its frozen 5125-line cap after this change — no rebaseline needed)
  • node scripts/check/check-dashboard-typecheck.mjs (touched src/app/(dashboard)/**; fixed one real bug this surfaced — notify.notify(...) doesn't exist on useNotificationStore, corrected to notify.error(...))
  • npm run check:cycles

What was deferred

  • Per-model UI row for the toggle (provider-level only in this pass; per-model overrides remain reachable via the API's models field).
  • fetchBackend/fetchProxyUrl pinning is wired end-to-end (resolved in webFetchExecution.ts from the interception-rules row) but not yet exposed in the UI toggle — only the boolean flags are.

No live/VPS validation needed — this is pure application logic covered by unit tests (no OAuth, no upstream handshake, no OS-level behavior).

Refs #7339

…peline (#7339)

Phases 3-4 of #3384 (Phases 1-2 shipped DB schema + resolveInterceptSearch in
release/v3.8.47). Adds resolveInterceptFetch(provider, model) as a structural
twin of resolveInterceptSearch, and open-sse/services/webFetchInterception.ts
(mirroring webSearchFallback.ts) to rewrite a provider-native web_fetch tool
declaration into a synthetic omniroute_web_fetch function tool. The synthetic
tool call is dispatched through the existing handleToolCallExecution path
(same as omniroute_web_search) to a new web_fetch builtin skill handler that
resolves credentials and calls handleWebFetch() against /v1/web/fetch.

Strictly opt-in: with no interceptFetch DB row configured (the default), the
outgoing request body is byte-identical to pre-change behavior — no heuristic
default bypass like the interceptSearch sibling, to guarantee zero overhead
when disabled (Hard Rule #20).

Also ships the dashboard toggle (owner decision, overriding the plan's
backend-only recommendation): ProviderInterceptionSection.tsx on the provider
detail page, backed by GET/PUT/DELETE /api/providers/[id]/interception-rules,
covering both interceptSearch and interceptFetch from one control.

chatCore.ts touch is minimal (frozen file): one resolver call + one
prepareWebFetchFallbackBody call mirroring the existing interceptSearch block,
plus threading provider/model into the existing handleToolCallExecution call.
@diegosouzapw
diegosouzapw merged commit a95da4a into release/v3.8.49 Jul 19, 2026
13 of 20 checks passed
@diegosouzapw
diegosouzapw deleted the feat/7339-intercept-fetch-wiring branch July 19, 2026 21:00
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