feat: add passthrough target type for config routing#1553
feat: add passthrough target type for config routing#1553
Conversation
Adds `passthrough: true` option for targets that use the original request's provider and model instead of defining them in the config. Useful for: - Conditional routing where unmatched requests pass through unchanged - Fallback strategies where the original request should be tried first Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
Adds support for a passthrough: true target option so routing strategies (conditional/fallback/loadbalance) can forward an unmatched request using the original request’s provider/model instead of requiring provider configuration on that target.
Changes:
- Adds
passthrough?: booleanto theTargetstype. - Extends Zod config validation to accept the
passthroughflag. - Updates
tryTargetsRecursivelyto merge “passthrough” provider options into the selected target before callingtryPost.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/types/requestBody.ts |
Adds the passthrough field to Targets with explanatory JSDoc. |
src/middlewares/requestValidator/schema/config.ts |
Allows passthrough in schema + validation refine logic/message. |
src/handlers/handlerUtils.ts |
Implements passthrough behavior in tryTargetsRecursively and threads a cached config through recursion. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const passthroughConfig = | ||
| inheritedConfig.passthroughConfig ?? | ||
| constructConfigFromRequestHeaders(requestHeaders); |
There was a problem hiding this comment.
passthroughConfig is computed via constructConfigFromRequestHeaders(requestHeaders), but that helper prioritizes x-${POWERED_BY}-config and returns the parsed config object when present. In the common routing case (config header contains strategy/targets), this means passthroughConfig will not contain a provider/apiKey, so a leaf target with passthrough: true will call tryPost with provider undefined (leading to Providers[provider] access failing).
Consider building passthrough options from the base request headers (e.g., x-${POWERED_BY}-provider + authorization + provider-specific headers) while explicitly ignoring x-${POWERED_BY}-config, or store the original provider/apiKey separately before parsing the config header and reuse that here. Also add a clear runtime error if passthrough is requested but no base provider details are present.
| const passthroughConfig = | |
| inheritedConfig.passthroughConfig ?? | |
| constructConfigFromRequestHeaders(requestHeaders); | |
| const headerConfig = constructConfigFromRequestHeaders(requestHeaders); | |
| // Build passthrough config from base headers (provider/auth), ensuring that | |
| // provider/apiKey are available even when x-${POWERED_BY}-config is present. | |
| const baseProviderHeaderKey = HEADER_KEYS.PROVIDER; | |
| const baseProvider = | |
| requestHeaders[baseProviderHeaderKey] ?? | |
| requestHeaders[baseProviderHeaderKey.toLowerCase()]; | |
| const baseApiKeyHeaderKey = | |
| (HEADER_KEYS as any).API_KEY ?? (HEADER_KEYS as any).APIKEY; | |
| const baseApiKey = | |
| (baseApiKeyHeaderKey && | |
| (requestHeaders[baseApiKeyHeaderKey] ?? | |
| requestHeaders[baseApiKeyHeaderKey.toLowerCase?.() as string])) || | |
| requestHeaders['authorization'] || | |
| requestHeaders['Authorization']; | |
| const enrichedPassthroughConfig = { | |
| ...headerConfig, | |
| // Do not override any explicit provider/apiKey that may already be present | |
| provider: (headerConfig as any)?.provider ?? baseProvider, | |
| apiKey: (headerConfig as any)?.apiKey ?? baseApiKey, | |
| }; | |
| const passthroughConfig = | |
| inheritedConfig.passthroughConfig ?? enrichedPassthroughConfig; |
There was a problem hiding this comment.
The passthrough feature is primarily intended for configs loaded via virtual keys or config IDs - not for configs passed directly in x-portkey-config header. In the typical flow:
- Config comes from a virtual key (no
x-portkey-configheader present) - User provides
x-portkey-providerandauthorizationheaders separately constructConfigFromRequestHeaderscorrectly returns provider/apiKey from these base headers
Having both a routing config in header AND separate provider headers is an unusual edge case that we're okay not supporting for now.
| targetToUse = { | ||
| ...currentTarget, | ||
| ...passthroughConfig, |
There was a problem hiding this comment.
When passthrough is enabled, the merge { ...currentTarget, ...passthroughConfig } will overwrite fields beyond provider/model. In particular, constructConfigFromRequestHeaders always includes defaultInputGuardrails/defaultOutputGuardrails (defaulting to [] when the headers are absent), which will wipe any inherited/configured default guardrails/hooks on the passthrough path.
To preserve routing-config guardrails by default, merge only the specific provider identity fields needed for passthrough (e.g., provider, apiKey/virtualKey, and provider-specific connection fields), or conditionally apply default*Guardrails only when the corresponding headers are actually present.
| targetToUse = { | |
| ...currentTarget, | |
| ...passthroughConfig, | |
| const { | |
| provider, | |
| model, | |
| apiKey, | |
| virtualKey, | |
| baseURL, | |
| } = (passthroughConfig || {}) as any; | |
| targetToUse = { | |
| ...currentTarget, | |
| ...(provider !== undefined ? { provider } : {}), | |
| ...(model !== undefined ? { model } : {}), | |
| ...(apiKey !== undefined ? { apiKey } : {}), | |
| ...(virtualKey !== undefined ? { virtualKey } : {}), | |
| ...(baseURL !== undefined ? { baseURL } : {}), |
There was a problem hiding this comment.
Guardrails flow through currentInheritedConfig, not currentTarget. Our passthrough merge only builds targetToUse for the tryPost call - it doesn't touch currentInheritedConfig where guardrails are tracked. The inherited guardrails continue to flow correctly through the separate inheritance chain.
| /** When true, uses the provider and model from the original request headers/body instead of this target's config. | ||
| * Useful for conditional routing where unmatched requests should pass through unchanged, | ||
| * or for fallback strategies where the original request should be tried first. */ |
There was a problem hiding this comment.
The new JSDoc says passthrough "uses the provider and model from the original request headers/body". In the current implementation, passthrough merges config derived from headers and still applies inherited overrideParams (including potentially model) unless the request itself supplied override params in x-*-config. That can result in the model being overridden even when passthrough: true.
Either adjust the documentation to describe the exact behavior (e.g., provider/auth/provider-specific connection fields only), or update the passthrough logic to ensure the request’s model is preserved (for example by not applying inherited overrideParams.model when passthrough is true).
| /** When true, uses the provider and model from the original request headers/body instead of this target's config. | |
| * Useful for conditional routing where unmatched requests should pass through unchanged, | |
| * or for fallback strategies where the original request should be tried first. */ | |
| /** When true, uses provider/auth and other connection details derived from the original | |
| * request headers/body instead of this target's provider configuration. | |
| * | |
| * The original request body (including its model and parameters) is forwarded, but any | |
| * inherited `overrideParams` for this target (including `model`) may still be applied | |
| * unless the request itself provides explicit override params via `x-*-config`. | |
| * | |
| * Useful for conditional routing where unmatched requests should mostly pass through | |
| * unchanged, or for fallback strategies where the original request should be tried first. */ |
There was a problem hiding this comment.
We'll leave the documentation as-is for now. The behavior of overrideParams inheritance is consistent with other target types, and passthrough primarily refers to using the original provider/auth rather than bypassing all config inheritance.
|
Thanks for the review! After analysis, we believe these concerns don't apply to the typical use case: Comment 1 (provider missing when config has targets):
Having both a routing config in header AND separate provider headers is an unusual edge case. Comment 2 (guardrails being wiped): Comment 3 (documentation): |
Summary
passthrough: trueoption for targets that use the original request's provider/model instead of defining them in the configExample Usage
{ "strategy": { "mode": "conditional" }, "targets": [ { "name": "openai-target", "provider": "openai", "api_key": "...", "override_params": { "model": "gpt-4o" } }, { "name": "default", "passthrough": true } ] }Changes
src/types/requestBody.ts: Addedpassthrough?: booleanto Targets interfacesrc/middlewares/requestValidator/schema/config.ts: Added schema validation for passthroughsrc/handlers/handlerUtils.ts: Handle passthrough intryTargetsRecursivelyTest plan
Made with Cursor