feat(core): expose request headers to dynamic agents#1218
Conversation
🦋 Changeset detectedLatest commit: 081f16d The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This comment has been minimized.
This comment has been minimized.
📝 WalkthroughWalkthroughExposes per-call HTTP request headers (normalized to lowercase) to dynamic agent resolvers by adding Changes
Sequence Diagram(s)sequenceDiagram
participant HTTP as HTTP Request
participant Route as Route Handler
participant Handler as Server Handler
participant Processor as Options Processor
participant Agent as Agent Engine
participant Resolver as Dynamic Resolver
HTTP->>Route: incoming request (with headers)
Route->>Handler: call handleGenerate*(..., requestHeaders)
Handler->>Processor: processAgentOptions(body, signal, requestHeaders)
Processor->>Processor: normalizeRequestHeaders() (lowercase keys)
Processor-->>Handler: ProcessedAgentOptions (with requestHeaders)
Handler->>Agent: agent.generateText(..., { requestHeaders })
Agent->>Agent: createOperationContext({ requestHeaders })
Agent->>Resolver: resolve dynamic model/instructions/tools (options.headers = oc.requestHeaders)
Resolver-->>Agent: model/tools result
Agent-->>Route: response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/serverless-hono/src/routes.ts (1)
215-240: Same structural concern as in Elysia/Hono routes — downstream impact noted at the Elysia comment.
handleGenerateText/etc. already accept a typedrequestHeaders6th parameter that runs throughnormalizeRequestHeadersinpackages/server-core/src/utils/options.ts. The nested callwithRequestHeadersInOptions(withServerlessEnvInOptions(body, runtimeEnv), c.req.raw.headers)(lines 326, 344, 362, 392, 410) instead injects the raw record intobody.options, which:
- bypasses
normalizeRequestHeaders(you only get lowercased keys becauseHeaders.forEachhappens to lowercase per spec), and- is more spoofable here than in Elysia because
readJsonBodyis typedanywith no TypeBox schema to strip unknown fields — a client-suppliedoptions.requestHeadersin the JSON body will still reach dynamic resolvers wheneverc.req.raw.headersiterates empty (edge cases / synthetic requests).Consider passing
c.req.raw.headersas the 6th argument to each handler and removingwithRequestHeadersInOptions(keeping onlywithServerlessEnvInOptions).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/serverless-hono/src/routes.ts` around lines 215 - 240, withRequestHeadersInOptions injects raw headers into body.options and bypasses normalizeRequestHeaders (and allows client-supplied options.requestHeaders to persist); remove usage of withRequestHeadersInOptions and instead pass c.req.raw.headers as the sixth argument to the handler calls (e.g., handleGenerateText, handleEmbed, handleChat, handleModeration, handleOtherHandlers) so the handlers run normalizeRequestHeaders in packages/server-core/src/utils/options.ts; keep withServerlessEnvInOptions but stop merging requestHeaders into the body, and delete the withRequestHeadersInOptions helper and its call sites.packages/server-core/src/utils/options.ts (1)
15-18: Headers branch doesn't explicitly lowercase keys, unlike the Record branch.The Record branch (lines 21-27) explicitly lowercases keys, but the
Headersbranch relies on the Fetch-spec guarantee thatHeadersiteration yields lowercased names. This works today, but the asymmetry is surprising and will silently break if someone later refactors this to accept aHeaders-like polyfill that doesn't normalize. A one-line fix keeps the two branches consistent and makes the lowercase invariant explicit:if (typeof Headers !== "undefined" && headers instanceof Headers) { - const entries = Array.from(headers.entries()); - return entries.length > 0 ? Object.fromEntries(entries) : undefined; + const normalized: Record<string, string> = {}; + headers.forEach((value, key) => { + normalized[key.toLowerCase()] = value; + }); + return Object.keys(normalized).length > 0 ? normalized : undefined; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/server-core/src/utils/options.ts` around lines 15 - 18, The Headers branch currently converts headers via Array.from(headers.entries()) and Object.fromEntries without normalizing header names; update the Headers branch (the code handling the headers variable and entries) to explicitly lowercase header names before calling Object.fromEntries (e.g., map entries to [key.toLowerCase(), value]) so it matches the Record branch's behavior and preserves the lowercase invariant even for non-spec Headers-like inputs.packages/server-elysia/src/routes/agent.routes.ts (1)
67-93: Prefer passing request headers as a dedicated handler argument instead of mutatingbody.options.
handleGenerateText/handleStreamText/handleChatStream/handleGenerateObject/handleStreamObjectall accept a dedicatedrequestHeadersparameter (seepackages/server-core/src/handlers/agent.handlers.tsline 69 etc.) that is threaded intoprocessAgentOptions(body, signal, requestHeaders)and runs throughnormalizeRequestHeaders. By instead injectingrequestHeadersintobody.options, this route:
- Bypasses
normalizeRequestHeadersentirely — the route-merged record is just spread via...optionsinprocessAgentOptions. It happens to work only becauseHeaders.forEachalready yields lowercased keys per the Fetch spec, so the invariant is fragile and inconsistent with the Record-input normalization path.- Creates a minor spoofing surface: if an incoming request has zero headers, the
Object.keys(requestHeaders).length === 0short-circuit returnsbodyunchanged, allowing a client-suppliedoptions.requestHeadersin the JSON payload to flow through to dynamic resolvers. In practice Elysia'sTextRequestSchema/ObjectRequestSchemamay strip it, butserverless-hono/server-honoread the body asany, so the same helper pattern there is more exposed.- Duplicates logic with
normalizeRequestHeadersinpackages/server-core/src/utils/options.ts.Consider dropping
withRequestHeadersInOptionshere and passingrequest.headersas the 6th argument to each handler, letting server-core own normalization. Same applies to the identical helper inpackages/server-hono/src/routes/index.tsandpackages/serverless-hono/src/routes.ts.♻️ Proposed direction
- const response = await handleGenerateText( - params.id, - withRequestHeadersInOptions(body, request.headers), - deps, - logger, - request.signal, - ); + const response = await handleGenerateText( + params.id, + body, + deps, + logger, + request.signal, + request.headers, + );And remove
withRequestHeadersInOptionsentirely.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/server-elysia/src/routes/agent.routes.ts` around lines 67 - 93, The withRequestHeadersInOptions helper mutates body.options and bypasses normalizeRequestHeaders; remove calls to withRequestHeadersInOptions and delete the helper, and instead pass the incoming request.headers as the dedicated requestHeaders argument (6th parameter) into handleGenerateText, handleStreamText, handleChatStream, handleGenerateObject and handleStreamObject so server-core's processAgentOptions/normalizeRequestHeaders handle normalization; also remove the identical helper usages in the packages/server-hono and packages/serverless-hono route files so all routes consistently forward request.headers rather than injecting into body.options.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/server-hono/src/routes/index.ts`:
- Around line 67-92: Replace the unsafe any with unknown for the body parameter
in withRequestHeadersInOptions and add a type guard to safely narrow it to a
plain object: change the signature to withRequestHeadersInOptions(body: unknown,
headers: Headers), implement a small isPlainObject(value: unknown): value is
Record<string, unknown> that checks typeof value === "object" && value !== null
&& !Array.isArray(value), then use that guard before treating body as an object;
when reading body.options, narrow it with the same guard (e.g., const options =
isPlainObject(body.options) ? body.options : {}) and ensure requestHeaders is
typed Record<string, string> so the function returns a correctly typed object
without using any.
---
Nitpick comments:
In `@packages/server-core/src/utils/options.ts`:
- Around line 15-18: The Headers branch currently converts headers via
Array.from(headers.entries()) and Object.fromEntries without normalizing header
names; update the Headers branch (the code handling the headers variable and
entries) to explicitly lowercase header names before calling Object.fromEntries
(e.g., map entries to [key.toLowerCase(), value]) so it matches the Record
branch's behavior and preserves the lowercase invariant even for non-spec
Headers-like inputs.
In `@packages/server-elysia/src/routes/agent.routes.ts`:
- Around line 67-93: The withRequestHeadersInOptions helper mutates body.options
and bypasses normalizeRequestHeaders; remove calls to
withRequestHeadersInOptions and delete the helper, and instead pass the incoming
request.headers as the dedicated requestHeaders argument (6th parameter) into
handleGenerateText, handleStreamText, handleChatStream, handleGenerateObject and
handleStreamObject so server-core's processAgentOptions/normalizeRequestHeaders
handle normalization; also remove the identical helper usages in the
packages/server-hono and packages/serverless-hono route files so all routes
consistently forward request.headers rather than injecting into body.options.
In `@packages/serverless-hono/src/routes.ts`:
- Around line 215-240: withRequestHeadersInOptions injects raw headers into
body.options and bypasses normalizeRequestHeaders (and allows client-supplied
options.requestHeaders to persist); remove usage of withRequestHeadersInOptions
and instead pass c.req.raw.headers as the sixth argument to the handler calls
(e.g., handleGenerateText, handleEmbed, handleChat, handleModeration,
handleOtherHandlers) so the handlers run normalizeRequestHeaders in
packages/server-core/src/utils/options.ts; keep withServerlessEnvInOptions but
stop merging requestHeaders into the body, and delete the
withRequestHeadersInOptions helper and its call sites.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ce78f77f-b70d-46ac-b67d-dbc5357bc8f2
📒 Files selected for processing (14)
.changeset/expose-request-headers-dynamic-agents.mdpackages/core/src/agent/agent.spec-d.tspackages/core/src/agent/agent.spec.tspackages/core/src/agent/agent.tspackages/core/src/agent/types.tspackages/core/src/voltops/types.tspackages/server-core/src/handlers/agent.handlers.spec.tspackages/server-core/src/handlers/agent.handlers.tspackages/server-core/src/utils/options.tspackages/server-elysia/src/routes/agent.routes.tspackages/server-hono/src/routes/index.tspackages/serverless-hono/src/routes.tswebsite/docs/agents/dynamic-agents.mdwebsite/docs/agents/prompts.md
| function withRequestHeadersInOptions(body: any, headers: Headers) { | ||
| if (!body || typeof body !== "object" || Array.isArray(body)) { | ||
| return body; | ||
| } | ||
|
|
||
| const requestHeaders: Record<string, string> = {}; | ||
| headers.forEach((value, key) => { | ||
| requestHeaders[key] = value; | ||
| }); | ||
| if (Object.keys(requestHeaders).length === 0) { | ||
| return body; | ||
| } | ||
|
|
||
| const options = | ||
| body.options && typeof body.options === "object" && !Array.isArray(body.options) | ||
| ? body.options | ||
| : {}; | ||
|
|
||
| return { | ||
| ...body, | ||
| options: { | ||
| ...options, | ||
| requestHeaders, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP 'function\s+withRequestHeadersInOptions\s*\(\s*body:\s*any\b' packages/server-hono/src/routes/index.tsRepository: VoltAgent/voltagent
Length of output: 133
Replace any with unknown and add type guard for type safety.
The body parameter at line 67 uses any, which bypasses TypeScript type checking for untrusted JSON input at the request boundary. This violates the type-safety guideline for the codebase.
♻️ Proposed refactor
-function withRequestHeadersInOptions(body: any, headers: Headers) {
- if (!body || typeof body !== "object" || Array.isArray(body)) {
+type JsonRecord = Record<string, unknown>;
+
+function isJsonRecord(value: unknown): value is JsonRecord {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+}
+
+function withRequestHeadersInOptions(body: unknown, headers: Headers) {
+ if (!isJsonRecord(body)) {
return body;
}
const requestHeaders: Record<string, string> = {};
headers.forEach((value, key) => {
@@
- const options =
- body.options && typeof body.options === "object" && !Array.isArray(body.options)
- ? body.options
- : {};
+ const options = isJsonRecord(body.options) ? body.options : {};
return {
...body,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/server-hono/src/routes/index.ts` around lines 67 - 92, Replace the
unsafe any with unknown for the body parameter in withRequestHeadersInOptions
and add a type guard to safely narrow it to a plain object: change the signature
to withRequestHeadersInOptions(body: unknown, headers: Headers), implement a
small isPlainObject(value: unknown): value is Record<string, unknown> that
checks typeof value === "object" && value !== null && !Array.isArray(value),
then use that guard before treating body as an object; when reading
body.options, narrow it with the same guard (e.g., const options =
isPlainObject(body.options) ? body.options : {}) and ensure requestHeaders is
typed Record<string, string> so the function returns a correctly typed object
without using any.
ef6cda1 to
081f16d
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/src/agent/agent.ts`:
- Line 4073: Normalize any direct-call request headers before storing: where the
code currently assigns requestHeaders: options?.requestHeaders ??
options?.parentOperationContext?.requestHeaders, transform the chosen headers
object so all header keys are lowercased (e.g., map keys to key.toLowerCase())
and use that normalized object instead. Update the assignment logic (referencing
requestHeaders, options, and parentOperationContext) to call the normalizer so
downstream access like headers["x-tenant-id"] is consistent across entrypoints.
In `@packages/core/src/agent/types.ts`:
- Around line 1081-1082: OperationContext currently stores
options?.requestHeaders verbatim which allows mixed-case keys from direct
agent.generateText calls; normalize header names to a canonical form (e.g.,
lower-case keys) before assigning to OperationContext so
DynamicValueOptions.headers always sees consistent casing. Update the code paths
that set OperationContext.requestHeaders (references: OperationContext,
agent.generateText, and where DynamicValueOptions.headers is read) to run a
small normalization helper that iterates options.requestHeaders and produces a
new Record<string,string> with normalized header keys, and apply the same
normalization in the other location mentioned (lines referencing the other
assignment around 1312-1313) so both HTTP/server-core flows and direct
in-process calls present headers with consistent casing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e7087e77-613c-490e-bfe9-02b35086d1f9
📒 Files selected for processing (14)
.changeset/expose-request-headers-dynamic-agents.mdpackages/core/src/agent/agent.spec-d.tspackages/core/src/agent/agent.spec.tspackages/core/src/agent/agent.tspackages/core/src/agent/types.tspackages/core/src/voltops/types.tspackages/server-core/src/handlers/agent.handlers.spec.tspackages/server-core/src/handlers/agent.handlers.tspackages/server-core/src/utils/options.tspackages/server-elysia/src/routes/agent.routes.tspackages/server-hono/src/routes/index.tspackages/serverless-hono/src/routes.tswebsite/docs/agents/dynamic-agents.mdwebsite/docs/agents/prompts.md
✅ Files skipped from review due to trivial changes (3)
- packages/core/src/voltops/types.ts
- .changeset/expose-request-headers-dynamic-agents.md
- website/docs/agents/dynamic-agents.md
🚧 Files skipped from review as they are similar to previous changes (6)
- website/docs/agents/prompts.md
- packages/server-elysia/src/routes/agent.routes.ts
- packages/core/src/agent/agent.spec-d.ts
- packages/server-core/src/handlers/agent.handlers.spec.ts
- packages/server-core/src/utils/options.ts
- packages/server-core/src/handlers/agent.handlers.ts
| return { | ||
| operationId, | ||
| context, | ||
| requestHeaders: options?.requestHeaders ?? options?.parentOperationContext?.requestHeaders, |
There was a problem hiding this comment.
Normalize direct-call requestHeaders before storing them.
Server adapters may already lowercase names, but direct in-process calls can pass mixed-case keys. Store normalized keys here so headers["x-tenant-id"] behaves consistently across all entrypoints.
🐛 Proposed fix
- requestHeaders: options?.requestHeaders ?? options?.parentOperationContext?.requestHeaders,
+ requestHeaders:
+ options?.requestHeaders !== undefined
+ ? Object.fromEntries(
+ Object.entries(options.requestHeaders).map(([name, value]) => [
+ name.toLowerCase(),
+ value,
+ ]),
+ )
+ : options?.parentOperationContext?.requestHeaders,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| requestHeaders: options?.requestHeaders ?? options?.parentOperationContext?.requestHeaders, | |
| requestHeaders: | |
| options?.requestHeaders !== undefined | |
| ? Object.fromEntries( | |
| Object.entries(options.requestHeaders).map(([name, value]) => [ | |
| name.toLowerCase(), | |
| value, | |
| ]), | |
| ) | |
| : options?.parentOperationContext?.requestHeaders, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/src/agent/agent.ts` at line 4073, Normalize any direct-call
request headers before storing: where the code currently assigns requestHeaders:
options?.requestHeaders ?? options?.parentOperationContext?.requestHeaders,
transform the chosen headers object so all header keys are lowercased (e.g., map
keys to key.toLowerCase()) and use that normalized object instead. Update the
assignment logic (referencing requestHeaders, options, and
parentOperationContext) to call the normalizer so downstream access like
headers["x-tenant-id"] is consistent across entrypoints.
| // HTTP request headers associated with this generation call, when available. | ||
| requestHeaders?: Record<string, string>; |
There was a problem hiding this comment.
Normalize direct-call requestHeaders before storing them on OperationContext.
Server adapters normalize through server-core, but direct agent.generateText(..., { requestHeaders }) can pass mixed-case keys. Since agent.ts stores options?.requestHeaders directly before exposing it as DynamicValueOptions.headers, dynamic resolvers can see inconsistent casing across HTTP vs in-process calls.
🐛 Proposed implementation shape
+function normalizeRequestHeaders(
+ headers?: Record<string, string>,
+): Record<string, string> | undefined {
+ if (!headers) {
+ return undefined;
+ }
+
+ return Object.fromEntries(
+ Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]),
+ );
+}
+
// In createOperationContext / operation context construction:
- requestHeaders: options?.requestHeaders ?? options?.parentOperationContext?.requestHeaders,
+ requestHeaders: normalizeRequestHeaders(
+ options?.requestHeaders ?? options?.parentOperationContext?.requestHeaders,
+ ),Also applies to: 1312-1313
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/src/agent/types.ts` around lines 1081 - 1082, OperationContext
currently stores options?.requestHeaders verbatim which allows mixed-case keys
from direct agent.generateText calls; normalize header names to a canonical form
(e.g., lower-case keys) before assigning to OperationContext so
DynamicValueOptions.headers always sees consistent casing. Update the code paths
that set OperationContext.requestHeaders (references: OperationContext,
agent.generateText, and where DynamicValueOptions.headers is read) to run a
small normalization helper that iterates options.requestHeaders and produces a
new Record<string,string> with normalized header keys, and apply the same
normalization in the other location mentioned (lines referencing the other
assignment around 1312-1313) so both HTTP/server-core flows and direct
in-process calls present headers with consistent casing.
PR Checklist
Please check if your PR fulfills the following requirements:
Bugs / Features
What is the current behavior?
Dynamic
instructions,model, andtoolsfunctions receive runtimecontextandprompts, but calls made through the built-in HTTP endpoints do not expose the incoming request headers. Users have to copy auth, tenant, or user headers intooptions.contextmanually.What is the new behavior?
Dynamic agent configuration now receives a
headersmap viaDynamicValueOptionswhen request headers are available. The Hono, Elysia, and serverless Hono agent routes attach incoming request headers asoptions.requestHeaders, and core exposes them to dynamicinstructions,model, andtoolswithout passingrequestHeadersthrough to the AI SDK call.Also documents
headersand direct in-processrequestHeadersusage, and adds tests for both core dynamic resolution and server-core option processing.fixes #1201
Notes for reviewers
Validation run:
pnpm --filter @voltagent/core typecheckpnpm --filter @voltagent/server-core typecheckpnpm --filter @voltagent/server-core test -- src/handlers/agent.handlers.spec.tspnpm --filter @voltagent/core test:single -- src/agent/agent.spec.tsAdapter package typecheck commands still report existing route typing issues unrelated to this change; the new handler argument compatibility errors were avoided by injecting
requestHeadersinto request options at the adapter layer.Summary by CodeRabbit
Summary by cubic
Expose incoming HTTP request headers to dynamic agent configuration so
instructions,model, andtoolscan be tenant/auth-aware without manual header copying. Headers are normalized to lowercase and are not sent to the AI provider.@voltagent/core: AddedrequestHeadersto generation options; exposed asheadersinDynamicValueOptionsfor dynamicinstructions,model, andtools.requestHeadersare stripped before provider calls.@voltagent/server-core: Handlers accept raw request headers and normalize them to lowercase (supportsHeadersand Node/edge dicts) viaprocessAgentOptions.@voltagent/server-hono,@voltagent/server-elysia,@voltagent/serverless-hono: Routes forward incoming request headers to server-core handlers across text, stream, chat, and object endpoints.requestHeadersin options.Written for commit 081f16d. Summary will update on new commits.