Skip to content

feat(core): expose request headers to dynamic agents#1218

Merged
omeraplak merged 1 commit intomainfrom
feat/dynamic-agent-request-headers
Apr 22, 2026
Merged

feat(core): expose request headers to dynamic agents#1218
omeraplak merged 1 commit intomainfrom
feat/dynamic-agent-request-headers

Conversation

@omeraplak
Copy link
Copy Markdown
Member

@omeraplak omeraplak commented Apr 22, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

Dynamic instructions, model, and tools functions receive runtime context and prompts, 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 into options.context manually.

What is the new behavior?

Dynamic agent configuration now receives a headers map via DynamicValueOptions when request headers are available. The Hono, Elysia, and serverless Hono agent routes attach incoming request headers as options.requestHeaders, and core exposes them to dynamic instructions, model, and tools without passing requestHeaders through to the AI SDK call.

Also documents headers and direct in-process requestHeaders usage, and adds tests for both core dynamic resolution and server-core option processing.

fixes #1201

Notes for reviewers

Validation run:

  • pnpm --filter @voltagent/core typecheck
  • pnpm --filter @voltagent/server-core typecheck
  • pnpm --filter @voltagent/server-core test -- src/handlers/agent.handlers.spec.ts
  • pnpm --filter @voltagent/core test:single -- src/agent/agent.spec.ts

Adapter package typecheck commands still report existing route typing issues unrelated to this change; the new handler argument compatibility errors were avoided by injecting requestHeaders into request options at the adapter layer.

Summary by CodeRabbit

  • New Features
    • Request headers are now provided to dynamic agent resolvers (instructions, model, tools), normalized to lowercase, enabling tenant- or user-aware model/tool selection. Callers can also pass requestHeaders programmatically to drive the same behavior.
  • Documentation
    • Docs updated with examples and guidance for header-driven dynamic configuration.
  • Tests
    • Unit tests added to validate header normalization and propagation to dynamic resolvers.

Summary by cubic

Expose incoming HTTP request headers to dynamic agent configuration so instructions, model, and tools can be tenant/auth-aware without manual header copying. Headers are normalized to lowercase and are not sent to the AI provider.

  • New Features
    • @voltagent/core: Added requestHeaders to generation options; exposed as headers in DynamicValueOptions for dynamic instructions, model, and tools. requestHeaders are stripped before provider calls.
    • @voltagent/server-core: Handlers accept raw request headers and normalize them to lowercase (supports Headers and Node/edge dicts) via processAgentOptions.
    • @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.
    • Direct in-process calls can pass requestHeaders in options.
    • Updated docs and tests to cover normalization and propagation.

Written for commit 081f16d. Summary will update on new commits.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 081f16d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@voltagent/core Patch
@voltagent/server-core Patch
@voltagent/server-hono Patch
@voltagent/server-elysia Patch
@voltagent/serverless-hono Patch

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

@joggrbot

This comment has been minimized.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Exposes per-call HTTP request headers (normalized to lowercase) to dynamic agent resolvers by adding headers to DynamicValueOptions and requestHeaders to generation options/operation context; HTTP route handlers and server utils forward and normalize headers into agent invocations and docs/changeset updated.

Changes

Cohort / File(s) Summary
Type System Extensions
packages/core/src/agent/types.ts, packages/core/src/voltops/types.ts
Added requestHeaders?: Record<string,string> to generate/operation option interfaces and headers?: Record<string,string> to DynamicValueOptions.
Core Agent Logic & Tests
packages/core/src/agent/agent.ts, packages/core/src/agent/agent.spec.ts, packages/core/src/agent/agent.spec-d.ts
Threaded requestHeaders through OperationContext and into dynamic value resolution so dynamic model, instructions, and tools callbacks receive options.headers; added unit tests and type-test assertions verifying propagation.
Server Core Utilities & Handlers
packages/server-core/src/handlers/agent.handlers.ts, packages/server-core/src/utils/options.ts, packages/server-core/src/handlers/agent.handlers.spec.ts
Handlers accept an optional requestHeaders argument; processAgentOptions and ProcessedAgentOptions accept/return normalized requestHeaders; added normalization helper and tests for header lowercasing.
HTTP Framework Integrations
packages/server-hono/src/routes/index.ts, packages/server-elysia/src/routes/agent.routes.ts, packages/serverless-hono/src/routes.ts
Route handlers now extract incoming request headers and pass them to server-core handlers (using framework-specific extraction). Minor extraction change for Headers in serverless route helper.
Docs & Changeset
website/docs/agents/dynamic-agents.md, website/docs/agents/prompts.md, .changeset/expose-request-headers-dynamic-agents.md
Documented headers availability in DynamicValueOptions, added examples showing tenant/auth header usage, and included a changeset describing the feature for patch releases.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

🐰 I hopped the headers through the stack,
Lowercased keys and kept the track,
From HTTP breeze to resolver's den,
Tenant, auth, and tools — all seen again,
A little hop for every agent friend.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% 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
Title check ✅ Passed The PR title clearly and concisely summarizes the main feature: exposing request headers to dynamic agents, which aligns with the primary changes throughout the codebase.
Linked Issues check ✅ Passed The PR fully addresses issue #1201's objective by exposing request headers to dynamic agent functions via a first-class headers parameter in DynamicValueOptions for model, tools, and instructions, eliminating the need for manual header-to-context copying.
Out of Scope Changes check ✅ Passed All changes are directly scoped to exposing request headers to dynamic agents: core type/implementation updates, server adapter integrations, tests, documentation, and changesets. No unrelated modifications were introduced.
Description check ✅ Passed The PR description fully addresses the template requirements with clear current/new behavior, linked issues, added tests, docs, changesets, and detailed reviewer notes.

✏️ 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 feat/dynamic-agent-request-headers

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.

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 typed requestHeaders 6th parameter that runs through normalizeRequestHeaders in packages/server-core/src/utils/options.ts. The nested call withRequestHeadersInOptions(withServerlessEnvInOptions(body, runtimeEnv), c.req.raw.headers) (lines 326, 344, 362, 392, 410) instead injects the raw record into body.options, which:

  • bypasses normalizeRequestHeaders (you only get lowercased keys because Headers.forEach happens to lowercase per spec), and
  • is more spoofable here than in Elysia because readJsonBody is typed any with no TypeBox schema to strip unknown fields — a client-supplied options.requestHeaders in the JSON body will still reach dynamic resolvers whenever c.req.raw.headers iterates empty (edge cases / synthetic requests).

Consider passing c.req.raw.headers as the 6th argument to each handler and removing withRequestHeadersInOptions (keeping only withServerlessEnvInOptions).

🤖 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 Headers branch relies on the Fetch-spec guarantee that Headers iteration yields lowercased names. This works today, but the asymmetry is surprising and will silently break if someone later refactors this to accept a Headers-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 mutating body.options.

handleGenerateText/handleStreamText/handleChatStream/handleGenerateObject/handleStreamObject all accept a dedicated requestHeaders parameter (see packages/server-core/src/handlers/agent.handlers.ts line 69 etc.) that is threaded into processAgentOptions(body, signal, requestHeaders) and runs through normalizeRequestHeaders. By instead injecting requestHeaders into body.options, this route:

  1. Bypasses normalizeRequestHeaders entirely — the route-merged record is just spread via ...options in processAgentOptions. It happens to work only because Headers.forEach already yields lowercased keys per the Fetch spec, so the invariant is fragile and inconsistent with the Record-input normalization path.
  2. Creates a minor spoofing surface: if an incoming request has zero headers, the Object.keys(requestHeaders).length === 0 short-circuit returns body unchanged, allowing a client-supplied options.requestHeaders in the JSON payload to flow through to dynamic resolvers. In practice Elysia's TextRequestSchema/ObjectRequestSchema may strip it, but serverless-hono/server-hono read the body as any, so the same helper pattern there is more exposed.
  3. Duplicates logic with normalizeRequestHeaders in packages/server-core/src/utils/options.ts.

Consider dropping withRequestHeadersInOptions here and passing request.headers as the 6th argument to each handler, letting server-core own normalization. Same applies to the identical helper in packages/server-hono/src/routes/index.ts and packages/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 withRequestHeadersInOptions entirely.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between fbbdc9e and ef6cda1.

📒 Files selected for processing (14)
  • .changeset/expose-request-headers-dynamic-agents.md
  • packages/core/src/agent/agent.spec-d.ts
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/types.ts
  • packages/core/src/voltops/types.ts
  • packages/server-core/src/handlers/agent.handlers.spec.ts
  • packages/server-core/src/handlers/agent.handlers.ts
  • packages/server-core/src/utils/options.ts
  • packages/server-elysia/src/routes/agent.routes.ts
  • packages/server-hono/src/routes/index.ts
  • packages/serverless-hono/src/routes.ts
  • website/docs/agents/dynamic-agents.md
  • website/docs/agents/prompts.md

Comment on lines +67 to +92
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,
},
};
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'function\s+withRequestHeadersInOptions\s*\(\s*body:\s*any\b' packages/server-hono/src/routes/index.ts

Repository: 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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 14 files

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 22, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: 081f16d
Status:⚡️  Build in progress...

View logs

@omeraplak omeraplak force-pushed the feat/dynamic-agent-request-headers branch from ef6cda1 to 081f16d Compare April 22, 2026 00:58
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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between ef6cda1 and 081f16d.

📒 Files selected for processing (14)
  • .changeset/expose-request-headers-dynamic-agents.md
  • packages/core/src/agent/agent.spec-d.ts
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/types.ts
  • packages/core/src/voltops/types.ts
  • packages/server-core/src/handlers/agent.handlers.spec.ts
  • packages/server-core/src/handlers/agent.handlers.ts
  • packages/server-core/src/utils/options.ts
  • packages/server-elysia/src/routes/agent.routes.ts
  • packages/server-hono/src/routes/index.ts
  • packages/serverless-hono/src/routes.ts
  • website/docs/agents/dynamic-agents.md
  • website/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,
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 | 🟡 Minor

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.

Suggested change
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.

Comment on lines +1081 to +1082
// HTTP request headers associated with this generation call, when available.
requestHeaders?: Record<string, string>;
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

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.

@omeraplak omeraplak merged commit 4860832 into main Apr 22, 2026
22 of 23 checks passed
@omeraplak omeraplak deleted the feat/dynamic-agent-request-headers branch April 22, 2026 01:04
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.

[FEAT] Expose request headers (or a headers map) in dynamic agent model / tools construction

1 participant