Checked other resources
Example Code
import { ChatOpenRouter } from "@langchain/openrouter";
const model = new ChatOpenRouter({
model: "deepseek/deepseek-v4-flash-0731",
apiKey: process.env.OPENROUTER_API_KEY,
});
const res = await model.invoke("hi");
console.log(res.response_metadata);
// {
// model: 'deepseek/deepseek-v4-flash-0731',
// model_provider: 'openrouter', // integration name, not the upstream
// model_name: 'deepseek/deepseek-v4-flash-0731',
// finish_reason: 'stop'
// }
// expected: a `provider` key naming the upstream that served the call
// The same call over raw fetch does carry it:
const raw = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "deepseek/deepseek-v4-flash-0731",
messages: [{ role: "user", content: "hi" }],
}),
}).then((r) => r.json());
console.log(raw.provider); // "DigitalOcean"
console.log(raw.usage); // includes `cost`, also absent from response_metadata
Error Message and Stack Trace (if applicable)
No response
Description
Prior art
The closest precedent I found is #7335 (model_name missing in response_metadata in the JS SDK while present in Python — same parity gap, different field). No open issue covers the provider field for @langchain/openrouter.
The problem
OpenRouter's chat completions response includes a top-level provider field naming the upstream that actually served the call. Verified live (non-streaming):
{
"id": "gen-1234567891-qwertyuiop",
"model": "deepseek/deepseek-v4-flash-0731",
"provider": "DigitalOcean",
"choices": [...],
"usage": { "cost": 0.000003079972, ... }
}
ChatOpenRouter drops it. In libs/providers/langchain-openrouter/src/converters/messages.ts, convertOpenRouterResponseToBaseMessage builds:
message.response_metadata = {
...message.response_metadata,
model: rawResponse.model,
model_provider: "openrouter", // hardcoded integration name
model_name: rawResponse.model,
finish_reason: choice.finish_reason,
};
rawResponse.provider is never read. _generate returns llmOutput: { tokenUsage: data.usage } only, so the field is unreachable from callbacks too.
Why there is no workaround inside the library
- No custom
fetch injection point — _generate/_stream call global fetch directly.
- The
includeRawResponse → additional_kwargs.__raw_response path in @langchain/openai's completions converter exists, but ChatOpenRouter's converter doesn't pass it through.
- The package's
exports map only exposes ".", so the converters can't be imported to subclass _generate cleanly.
The only workaround today is GET /api/v1/generation?id=<message.id> after every completion — an extra HTTP round-trip per call to re-fetch a field the API already delivered in the response body, with rate-limit exposure at scale (and there is no bulk generation-stats endpoint).
Parity: the Python package already does this
langchain-openrouter (Python) surfaces it, in _create_chat_result:
if isinstance(message, AIMessage):
if provider:
message.response_metadata["provider"] = provider
Its response_metadata also carries cost, cost_details and native_finish_reason. The JS package exposes none of these.
Why it matters
OpenRouter routes each request to one of several upstreams, and the upstream determines the real price, latency, quantization and quality of the answer. Cost attribution and per-provider quality debugging both need to know which upstream answered — per call, not from the daily-aggregate /activity ledger.
Proposed change
Copy the field in the non-streaming converter:
message.response_metadata = {
...message.response_metadata,
model: rawResponse.model,
model_provider: "openrouter",
model_name: rawResponse.model,
finish_reason: choice.finish_reason,
provider: rawResponse.provider, // upstream that served the call
};
For streaming, the SSE chunks carry provider too, but since chunk response_metadata strings concatenate under the merge rules, it should be set once (e.g. on the usage-bearing/final chunk), mirroring what the Python _stream does via generation_info.
Happy to open a PR if the approach sounds right.
System Info
@langchain/openrouter: verified on 0.4.5 and 0.4.10 (latest at time of filing — same behavior in both)
@langchain/openai (transitive dependency): 1.5.5
Runtime: Bun 1.4.0 (also reproduced on Node v24.1.0)
Platform: reproduced on both Windows and Linux
Package manager: bun 1.4.0 (pnpm 10.33.2 also installed)
Checked other resources
Example Code
Error Message and Stack Trace (if applicable)
No response
Description
Prior art
The closest precedent I found is #7335 (
model_namemissing inresponse_metadatain the JS SDK while present in Python — same parity gap, different field). No open issue covers theproviderfield for@langchain/openrouter.The problem
OpenRouter's chat completions response includes a top-level
providerfield naming the upstream that actually served the call. Verified live (non-streaming):ChatOpenRouterdrops it. Inlibs/providers/langchain-openrouter/src/converters/messages.ts,convertOpenRouterResponseToBaseMessagebuilds:rawResponse.provideris never read._generatereturnsllmOutput: { tokenUsage: data.usage }only, so the field is unreachable from callbacks too.Why there is no workaround inside the library
fetchinjection point —_generate/_streamcall globalfetchdirectly.includeRawResponse→additional_kwargs.__raw_responsepath in@langchain/openai's completions converter exists, butChatOpenRouter's converter doesn't pass it through.exportsmap only exposes".", so the converters can't be imported to subclass_generatecleanly.The only workaround today is
GET /api/v1/generation?id=<message.id>after every completion — an extra HTTP round-trip per call to re-fetch a field the API already delivered in the response body, with rate-limit exposure at scale (and there is no bulk generation-stats endpoint).Parity: the Python package already does this
langchain-openrouter(Python) surfaces it, in_create_chat_result:Its
response_metadataalso carriescost,cost_detailsandnative_finish_reason. The JS package exposes none of these.Why it matters
OpenRouter routes each request to one of several upstreams, and the upstream determines the real price, latency, quantization and quality of the answer. Cost attribution and per-provider quality debugging both need to know which upstream answered — per call, not from the daily-aggregate
/activityledger.Proposed change
Copy the field in the non-streaming converter:
For streaming, the SSE chunks carry
providertoo, but since chunkresponse_metadatastrings concatenate under the merge rules, it should be set once (e.g. on the usage-bearing/final chunk), mirroring what the Python_streamdoes viageneration_info.Happy to open a PR if the approach sounds right.
System Info
@langchain/openrouter: verified on 0.4.5 and 0.4.10 (latest at time of filing — same behavior in both)
@langchain/openai (transitive dependency): 1.5.5
Runtime: Bun 1.4.0 (also reproduced on Node v24.1.0)
Platform: reproduced on both Windows and Linux
Package manager: bun 1.4.0 (pnpm 10.33.2 also installed)