Skip to content

feat(routing): add filtered providers and stripped params to routing metadata#1657

Open
steebchen wants to merge 4 commits intomainfrom
add-routing-metadata-param-filtering
Open

feat(routing): add filtered providers and stripped params to routing metadata#1657
steebchen wants to merge 4 commits intomainfrom
add-routing-metadata-param-filtering

Conversation

@steebchen
Copy link
Copy Markdown
Member

@steebchen steebchen commented Feb 13, 2026

Summary

  • Track which provider mappings were excluded during routing due to unsupported features (tools, vision, web_search, json_output, json_schema, reasoning, etc.) and surface the specific reasons in routing metadata
  • Track which request parameters (temperature, top_p, frequency_penalty, presence_penalty, max_tokens) were silently stripped because the selected provider doesn't support them
  • Display both filtered providers and stripped parameters in the log card and detailed log view UI

Test plan

  • Send a request with tools to a model where some providers don't support tools — verify filtered providers appear in routing metadata
  • Send a request with vision to a model where some providers lack vision support — verify filtered providers shown
  • Send a request with temperature to a provider that doesn't support it — verify strippedParameters includes "temperature"
  • Check log card UI shows "Filtered Providers" and "Stripped Parameters" sections
  • Check detailed log view shows the same sections

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Enhanced activity logs now display which providers were filtered out and the reasons why
    • Added tracking and visibility of request parameters unsupported by the selected provider
    • Improved routing information provides clearer insights into provider selection decisions and constraints

…ing metadata

Track which provider mappings were excluded during routing due to unsupported
features (tools, vision, web_search, json_output, etc.) and which request
parameters were stripped because the selected provider doesn't support them.
This provides visibility into why certain providers were not considered.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 13, 2026 09:27
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 236bb166-39c3-4c07-8fe2-8c4ef70ae3dd

📥 Commits

Reviewing files that changed from the base of the PR and between a4d4982 and a317e93.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/gateway/src/chat/chat.ts

Walkthrough

This PR adds provider filtering reason tracking and parameter stripping tracking throughout the routing system. A new getProviderFilterReasons() function evaluates provider capability constraints; filtering decisions are recorded in routingMetadata.filteredProviders and unsupported request parameters in routingMetadata.strippedParameters. Types, schema, core routing logic, and UI display are updated accordingly.

Changes

Provider Filtering & Parameter Stripping Observability

Layer / File(s) Summary
Type Definitions
packages/actions/src/get-cheapest-from-available-providers.ts
RoutingMetadata interface gains optional filteredProviders (array of { providerId, reasons }) and strippedParameters (array of strings) fields.
Database Schema
packages/db/src/schema.ts
Log table's routingMetadata JSON type signature extended to include filteredProviders and strippedParameters structures.
Core Routing Logic
apps/gateway/src/chat/chat.ts
New getProviderFilterReasons() function centralizes capability filtering checks (reasoning, tools, web search, images, JSON output, max tokens, etc.). Integrated into auto-routing (tracks selectedFilteredProviders), low-uptime fallback routing (tracks filteredOutProvidersFallback), and direct provider routing (tracks filteredOutProvidersDirect). Parameter stripping captures unsupported request fields in strippedParameters. All tracking is attached to routingMetadata when non-empty.
UI Display
apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx
New conditional sections render "Filtered Providers" (with Filter icon) and "Stripped Parameters" blocks, displaying provider IDs with reasons and unsupported parameters respectively.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • theopenco/llmgateway#2095: Modifies routing metadata and scoring logic in the same gateway routing files; both PRs enhance routing observability and decision tracking.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 summarizes the main objective of the pull request—adding filtered providers and stripped parameters tracking to routing metadata across the codebase.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-routing-metadata-param-filtering

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.

Caution

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

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/chat.ts (1)

1004-1038: ⚠️ Potential issue | 🟠 Major

Add reasoning flags to fallback/direct filter options.

Line 1008 and Line 1153 define filterOpts / directFilterOpts without reasoning fields, so getProviderFilterReasons won’t filter providers that lack reasoning support (or honor no_reasoning) in fallback/direct paths. This can route to unsupported providers and omit reasoning-related reasons in filteredProviders.

🛠️ Suggested fix
const filterOpts = {
	webSearchTool: !!webSearchTool,
	responseFormatType: response_format?.type,
	hasImages,
	hasTools: tools !== undefined || tool_choice !== undefined,
+	reasoningEffort: reasoning_effort,
+	reasoningMaxTokens: reasoning_max_tokens,
+	noReasoning: no_reasoning,
};

const directFilterOpts = {
	webSearchTool: !!webSearchTool,
	responseFormatType: response_format?.type,
	hasImages,
	hasTools: tools !== undefined || tool_choice !== undefined,
+	reasoningEffort: reasoning_effort,
+	reasoningMaxTokens: reasoning_max_tokens,
+	noReasoning: no_reasoning,
};

Also applies to: 1114-1116, 1149-1178, 1222-1224

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds richer routing introspection by recording (1) which provider mappings were excluded during routing and why, and (2) which request parameters were stripped due to provider capability limitations, then surfaces this data in the dashboard log UI.

Changes:

  • Extend routing metadata schema/types to include filteredProviders (with reasons) and strippedParameters.
  • Record filtered provider mappings during routing and record stripped request params based on supportedParameters.
  • Render the new routing metadata sections in the log card and log detail views.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/db/src/schema.ts Extends log.routingMetadata JSON shape with filteredProviders and strippedParameters.
packages/actions/src/get-cheapest-from-available-providers.ts Updates shared RoutingMetadata interface to include new optional fields.
apps/gateway/src/chat/chat.ts Collects filter reasons during routing and records stripped parameters into routing metadata.
apps/ui/src/components/dashboard/log-card.tsx Displays filtered providers + stripped parameters on the log card.
apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx Displays filtered providers + stripped parameters in the detailed log view.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

webSearchTool: !!webSearchTool,
responseFormatType: response_format?.type,
hasImages,
hasTools: tools !== undefined || tool_choice !== undefined,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

filterOpts.hasTools uses tools !== undefined, which is true even when tools is an empty array (e.g. web-search-only requests), and can incorrectly filter out providers with webSearch support but no function tools. Also, getProviderFilterReasons supports reasoning-related filtering, but this filterOpts omits reasoningEffort, reasoningMaxTokens, and noReasoning, so routing can still pick a provider mapping that can’t satisfy requested reasoning params. Compute hasTools from tools.length > 0 (plus tool_choice) and include the reasoning fields when calling getProviderFilterReasons.

Suggested change
hasTools: tools !== undefined || tool_choice !== undefined,
hasTools:
(Array.isArray(tools) && tools.length > 0) ||
tool_choice !== undefined,
reasoningEffort,
reasoningMaxTokens,
noReasoning,

Copilot uses AI. Check for mistakes.
webSearchTool: !!webSearchTool,
responseFormatType: response_format?.type,
hasImages,
hasTools: tools !== undefined || tool_choice !== undefined,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

directFilterOpts.hasTools uses tools !== undefined, which is true even when tools is an empty array (e.g. web-search-only requests), and can incorrectly filter out providers with webSearch support but no function tools. Additionally, this directFilterOpts doesn’t include reasoningEffort, reasoningMaxTokens, or noReasoning, so provider mappings can be selected that don’t support requested reasoning params. Compute hasTools from tools.length > 0 (plus tool_choice) and include the reasoning fields when calling getProviderFilterReasons.

Suggested change
hasTools: tools !== undefined || tool_choice !== undefined,
hasTools:
(Array.isArray(tools) && tools.length > 0) ||
tool_choice !== undefined,
reasoningEffort,
reasoningMaxTokens,
noReasoning,

Copilot uses AI. Check for mistakes.
Comment on lines +2033 to +2036
// Attach stripped parameters to routing metadata
if (strippedParameters.length > 0 && routingMetadata) {
routingMetadata.strippedParameters = strippedParameters;
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

New routingMetadata.strippedParameters behavior isn’t covered by the existing gateway tests (no matches for strippedParameters in apps/gateway/src tests). Add/extend an e2e/spec test that sends a request with an unsupported parameter for a provider (based on supportedParameters) and asserts the resulting log’s routingMetadata.strippedParameters contains the stripped param(s).

Copilot uses AI. Check for mistakes.
Comment on lines +782 to +786
const autoFilterOpts = {
webSearchTool: !!webSearchTool,
responseFormatType: response_format?.type,
hasImages,
hasTools: tools !== undefined || tool_choice !== undefined,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

hasTools is computed as tools !== undefined || tool_choice !== undefined, which treats an empty tools array as “tools requested”. After extracting the web_search tool, tools can be [] for web-search-only requests, which would incorrectly filter out providers that support webSearch but not function tools (search-only models). Compute hasFunctionTools = Array.isArray(tools) && tools.length > 0 and set hasTools from hasFunctionTools || tool_choice !== undefined.

Suggested change
const autoFilterOpts = {
webSearchTool: !!webSearchTool,
responseFormatType: response_format?.type,
hasImages,
hasTools: tools !== undefined || tool_choice !== undefined,
const hasFunctionTools = Array.isArray(tools) && tools.length > 0;
const hasTools = hasFunctionTools || tool_choice !== undefined;
const autoFilterOpts = {
webSearchTool: !!webSearchTool,
responseFormatType: response_format?.type,
hasImages,
hasTools,

Copilot uses AI. Check for mistakes.
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: 2

Caution

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

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/chat.ts (1)

431-438: 🛠️ Refactor suggestion | 🟠 Major

Avoid any/as any in web-search tool extraction; narrow the union type directly.

The tools parameter is a Zod-validated discriminated union of function and web_search types. You can remove both tool: any and as any by using a type guard on the type field to narrow the union.

Type-safe refactor
 	if (tools && Array.isArray(tools)) {
 		const webSearchToolIndex = tools.findIndex(
-			(tool: any) => tool.type === "web_search",
+			(tool) => tool.type === "web_search",
 		);
 		if (webSearchToolIndex !== -1) {
-			// Cast to any to access properties since the schema allows both function and web_search tools
-			const foundTool = tools[webSearchToolIndex] as any;
-			webSearchTool = {
-				type: "web_search",
-				user_location: foundTool.user_location,
-				search_context_size: foundTool.search_context_size,
-				max_uses: foundTool.max_uses,
-			};
+			const foundTool = tools[webSearchToolIndex];
+			if (foundTool.type === "web_search") {
+				webSearchTool = {
+					type: "web_search",
+					user_location: foundTool.user_location,
+					search_context_size: foundTool.search_context_size,
+					max_uses: foundTool.max_uses,
+				};
+			}
 			tools.splice(webSearchToolIndex, 1);
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 431 - 438, The current extraction
uses `any` and `as any` for `tools` items; replace that with a proper type guard
to narrow the Zod discriminated union so you don't need `any`. Create a
predicate like `isWebSearchTool(tool): tool is WebSearchTool` that checks
`tool.type === "web_search"`, use it with `Array.prototype.find` (or `findIndex`
+ array access) instead of `(tool: any)`, and then assign the result directly to
`foundTool`/`webSearchTool` (no `as any` casting). Ensure the `tools` parameter
is typed as the union type and use the guard so `foundTool`,
`webSearchToolIndex`, and `webSearchTool` are inferred with the concrete
`WebSearchTool` shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 2146-2149: The current attach of strippedParameters to
routingMetadata only happens once and can become stale after retries; update the
retry code paths so that after each call to resolveProviderContext (both in the
streaming and non-streaming retry loops) you merge that call's
strippedParameters into routingMetadata.strippedParameters (e.g., append/union
per-attempt values) instead of only setting it once, or modify
resolveProviderContext to return cumulative stripped params and assign that to
routingMetadata.strippedParameters; ensure you update all places that call
resolveProviderContext so routingMetadata.strippedParameters reflects every
attempt.
- Around line 805-813: The hasTools check currently treats an empty tools array
as true and over-filters providers; update the hasTools expression in
autoFilterOpts, filterOpts, and directFilterOpts to detect non-empty tool lists
by using (Array.isArray(tools) && tools.length > 0) || tool_choice !== undefined
so that a removed web_search (represented by an empty tools array) doesn’t count
as having tools; keep webSearchTool as its own flag (webSearchTool) and apply
this corrected hasTools logic to the hasTools fields in the objects named
autoFilterOpts, filterOpts, and directFilterOpts.

---

Outside diff comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 431-438: The current extraction uses `any` and `as any` for
`tools` items; replace that with a proper type guard to narrow the Zod
discriminated union so you don't need `any`. Create a predicate like
`isWebSearchTool(tool): tool is WebSearchTool` that checks `tool.type ===
"web_search"`, use it with `Array.prototype.find` (or `findIndex` + array
access) instead of `(tool: any)`, and then assign the result directly to
`foundTool`/`webSearchTool` (no `as any` casting). Ensure the `tools` parameter
is typed as the union type and use the guard so `foundTool`,
`webSearchToolIndex`, and `webSearchTool` are inferred with the concrete
`WebSearchTool` shape.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 94707c74-f6c0-42db-80d0-c1fecd8bf459

📥 Commits

Reviewing files that changed from the base of the PR and between aa3d827 and 15f3960.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts

Comment on lines +805 to +813
const autoFilterOpts = {
webSearchTool: !!webSearchTool,
responseFormatType: response_format?.type,
hasImages,
hasTools: tools !== undefined || tool_choice !== undefined,
reasoningEffort: reasoning_effort,
reasoningMaxTokens: reasoning_max_tokens,
noReasoning: no_reasoning,
};
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

hasTools currently over-filters providers when only web_search is used.

At Line 809 / Line 1057 / Line 1231, hasTools is derived from tools !== undefined. After removing the web_search tool from tools, an empty array still makes hasTools true, which can incorrectly add "tools not supported" and exclude valid providers.

Proposed fix
+	const hasFunctionTools =
+		Array.isArray(tools) && tools.some((tool) => tool.type === "function");

 	const autoFilterOpts = {
 		webSearchTool: !!webSearchTool,
 		responseFormatType: response_format?.type,
 		hasImages,
-		hasTools: tools !== undefined || tool_choice !== undefined,
+		hasTools:
+			hasFunctionTools ||
+			(tool_choice !== undefined && tool_choice !== "none"),
 		reasoningEffort: reasoning_effort,
 		reasoningMaxTokens: reasoning_max_tokens,
 		noReasoning: no_reasoning,
 	};

Apply the same hasTools expression to filterOpts and directFilterOpts.

Also applies to: 1053-1061, 1227-1235

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

In `@apps/gateway/src/chat/chat.ts` around lines 805 - 813, The hasTools check
currently treats an empty tools array as true and over-filters providers; update
the hasTools expression in autoFilterOpts, filterOpts, and directFilterOpts to
detect non-empty tool lists by using (Array.isArray(tools) && tools.length > 0)
|| tool_choice !== undefined so that a removed web_search (represented by an
empty tools array) doesn’t count as having tools; keep webSearchTool as its own
flag (webSearchTool) and apply this corrected hasTools logic to the hasTools
fields in the objects named autoFilterOpts, filterOpts, and directFilterOpts.

Comment on lines +2146 to +2149
// Attach stripped parameters to routing metadata
if (strippedParameters.length > 0 && routingMetadata) {
routingMetadata.strippedParameters = strippedParameters;
}
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

strippedParameters metadata can be stale after retry fallback switches provider.

Line 2147 attaches strippedParameters once, but retry paths call resolveProviderContext (see apps/gateway/src/chat/tools/resolve-provider-context.ts Line 215-Line 249), which strips parameters again per selected retry provider. Final logs can miss parameters stripped during retries.

Please merge per-attempt stripped parameters into routingMetadata.strippedParameters after each resolveProviderContext call (streaming and non-streaming retry loops), or return cumulative stripped params from resolveProviderContext.

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

In `@apps/gateway/src/chat/chat.ts` around lines 2146 - 2149, The current attach
of strippedParameters to routingMetadata only happens once and can become stale
after retries; update the retry code paths so that after each call to
resolveProviderContext (both in the streaming and non-streaming retry loops) you
merge that call's strippedParameters into routingMetadata.strippedParameters
(e.g., append/union per-attempt values) instead of only setting it once, or
modify resolveProviderContext to return cumulative stripped params and assign
that to routingMetadata.strippedParameters; ensure you update all places that
call resolveProviderContext so routingMetadata.strippedParameters reflects every
attempt.

steebchen and others added 2 commits March 18, 2026 17:42
Conflicts in apps/gateway/src/chat/chat.ts and apps/ui/src/components/dashboard/log-card.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

Resolved merge conflicts with origin/main:

apps/gateway/src/chat/chat.ts (8 conflicts):

  • Added PR's getProviderFilterReasons helper (extended with maxTokens check) alongside main's new utility functions (injectImageStreamParams, filterEligibleModelProviders, etc.)
  • Auto-routing loop: combined cachedFilteredProviders base (main) with filteredOutForModel tracking (PR)
  • Fallback section: kept PR's filteredOutProvidersFallback tracking, updated base to use preferConcreteRegionalMappings(expandedIamFilteredModelProviders) from main
  • Direct routing: added main's providerLockedRegions computation, kept PR's filteredOutProvidersDirect tracking with getProviderFilterReasons
  • Routing metadata: merged PR's filteredProviders with main's getNoFallbackRoutingMetadata, addContentFilterRoutingMetadata, and rate-limit annotations

apps/ui/src/components/dashboard/log-card.tsx (2 conflicts):

  • Took origin/main's SharedLogCard refactoring as the structural direction; the filteredProviders/strippedParameters UI display from this PR can be added to the shared component separately

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