Skip to content

ChatOpenRouter drops OpenRouter's top-level provider field from response_metadata (Python langchain-openrouter exposes it) #11509

Description

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a very descriptive title to this issue.
  • I searched the LangChain.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

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 includeRawResponseadditional_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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions