Checked other resources
Example Code
getHeadersWithUserAgent in libs/providers/langchain-openai/src/utils/azure.ts is used by every OpenAI and Azure OpenAI client to build defaultHeaders. When the caller supplies their own User-Agent, the result carries two user-agent entries:
import { getHeadersWithUserAgent } from "@langchain/openai/utils/azure";
const headers = getHeadersWithUserAgent({ "User-Agent": "my-app/1.2" });
console.log(headers);
// {
// "user-agent": "my-app/1.2",
// "User-Agent": "langchainjs-openai/1.0.0 ((node/v22.22.0; darwin; x64))"
// }
// What the transport actually sends:
console.log(new Headers(headers).get("user-agent"));
// "my-app/1.2, langchainjs-openai/1.0.0 ((node/v22.22.0; darwin; x64))"
Every OpenAI and Azure OpenAI client passes its defaultHeaders through this function before constructing the underlying SDK client, e.g. libs/providers/langchain-openai/src/chat_models/base.ts:
params.defaultHeaders = getHeadersWithUserAgent(params.defaultHeaders);
this.client = new OpenAIClient(params);
so anything set via configuration.defaultHeaders is affected:
new ChatOpenAI({
apiKey: "sk-test",
configuration: { defaultHeaders: { "User-Agent": "my-app/1.2" } },
});
Error Message and Stack Trace (if applicable)
No error is thrown — the header is silently malformed.
Description
The current implementation reads the caller's user agent back out of the normalized record using the canonical casing:
const normalizedHeaders = normalizeHeaders(headers);
// ...
return {
...normalizedHeaders,
"User-Agent": normalizedHeaders["User-Agent"]
? `${library}/${version} (${env})${normalizedHeaders["User-Agent"]}`
: `${library}/${version} (${env})`,
};
normalizeHeaders builds a WHATWG Headers instance and returns Object.fromEntries(output.entries()). Headers lowercases names on construction, so the record is keyed user-agent, never User-Agent. Three consequences:
- The lookup never matches.
normalizedHeaders["User-Agent"] is always undefined, so the ternary's truthy branch is unreachable — a caller's user agent is never appended, whatever casing they use.
- Two user-agent keys are emitted. The spread keeps the caller's
user-agent, and User-Agent is then added beside it. Both survive into defaultHeaders, and a transport collapses them into one comma-joined value. RFC 9110 §10.1.5 defines User-Agent as product tokens separated by whitespace, so the comma-joined form is malformed and breaks user-agent parsing/attribution on the provider side.
- The unreachable branch is also missing a separator. Were it reached, it would produce
langchainjs-openai/1.0.0 (env)my-app/1.2 with the caller's token glued to the closing parenthesis.
The intent of the ternary is clearly to prepend the library user agent to the caller's; it just never fires. This affects all six call sites — ChatOpenAI, AzureChatOpenAI, OpenAIEmbeddings, AzureOpenAIEmbeddings, OpenAI and AzureOpenAI LLMs — so it applies to plain OpenAI usage as well as Azure.
Expected: exactly one User-Agent header, with the caller's token appended after the library's and separated by a space.
System Info
platform: mac
node: v22.22.0
@langchain/openai: workspace (main @ fca7d2f8b)
Checked other resources
Example Code
getHeadersWithUserAgentinlibs/providers/langchain-openai/src/utils/azure.tsis used by every OpenAI and Azure OpenAI client to builddefaultHeaders. When the caller supplies their ownUser-Agent, the result carries two user-agent entries:Every OpenAI and Azure OpenAI client passes its
defaultHeadersthrough this function before constructing the underlying SDK client, e.g.libs/providers/langchain-openai/src/chat_models/base.ts:so anything set via
configuration.defaultHeadersis affected:Error Message and Stack Trace (if applicable)
No error is thrown — the header is silently malformed.
Description
The current implementation reads the caller's user agent back out of the normalized record using the canonical casing:
normalizeHeadersbuilds a WHATWGHeadersinstance and returnsObject.fromEntries(output.entries()).Headerslowercases names on construction, so the record is keyeduser-agent, neverUser-Agent. Three consequences:normalizedHeaders["User-Agent"]is alwaysundefined, so the ternary's truthy branch is unreachable — a caller's user agent is never appended, whatever casing they use.user-agent, andUser-Agentis then added beside it. Both survive intodefaultHeaders, and a transport collapses them into one comma-joined value. RFC 9110 §10.1.5 definesUser-Agentas product tokens separated by whitespace, so the comma-joined form is malformed and breaks user-agent parsing/attribution on the provider side.langchainjs-openai/1.0.0 (env)my-app/1.2with the caller's token glued to the closing parenthesis.The intent of the ternary is clearly to prepend the library user agent to the caller's; it just never fires. This affects all six call sites —
ChatOpenAI,AzureChatOpenAI,OpenAIEmbeddings,AzureOpenAIEmbeddings,OpenAIandAzureOpenAILLMs — so it applies to plain OpenAI usage as well as Azure.Expected: exactly one
User-Agentheader, with the caller's token appended after the library's and separated by a space.System Info