Checked other resources
Example Code
No API key needed — the converter is exported from the package root:
import { convertResponsesDeltaToChatGenerationChunk } from "@langchain/openai";
import { concat } from "@langchain/core/utils/stream";
// The two events around a streamed Responses call: `response.created` echoes
// the requested model, `response.completed` carries the resolved snapshot.
const events = [
{ type: "response.created", response: { id: "resp_1", model: "gpt-5.4" } },
{
type: "response.completed",
response: {
id: "resp_1",
model: "gpt-5.4-2026-03-05",
status: "completed",
output: [],
},
},
];
const merged = events
.map((e) => convertResponsesDeltaToChatGenerationChunk(e))
.filter((c) => c != null)
.map((c) => c.message)
.reduce((a, b) => concat(a, b));
console.log("model_name:", merged.response_metadata.model_name);
console.log("model: ", merged.response_metadata.model);
Actual output on @langchain/openai 1.5.10:
model_name: gpt-5.4
model: gpt-5.4gpt-5.4-2026-03-05
Expected: both gpt-5.4-2026-03-05. The non-streaming path already does this —
convertResponsesMessageToAIMessage allowlists model: response.model and
model_name: response.model from the completed response.
### Error Message and Stack Trace (if applicable)
_No response_
### Description
When the Responses API is used with streaming, `response_metadata.model_name` is the **requested** model string rather than the resolved snapshot the API actually served, and `response_metadata.model` is a concatenation of both.
In `convertResponsesDeltaToChatGenerationChunk` (`src/converters/responses.ts`), both keys are written from the `response.created` event, which echoes back the requested model:
```typescript
} else if (event.type === "response.created") {
id = event.response.id;
response_metadata.id = event.response.id;
response_metadata.model_name = event.response.model; // requested alias
response_metadata.model = event.response.model;
}
The response.completed branch carries the resolved snapshot, but only copies the keys present on its response object:
for (const [key, value] of Object.entries(event.response)) { ... }
event.response has no model_name key, so model_name is never corrected. model is written a second time — and because neither model nor model_name appears in the last-write-wins list _mergeDicts consults (["id", "name", "output_version", "model_provider"]) nor in DEFAULT_MERGE_IGNORE_KEYS, the second write is string-appended instead of replacing:
model_name: "gpt-5.4"
model: "gpt-5.4gpt-5.4-2026-03-05"
Expected: both "gpt-5.4-2026-03-05", matching the non-streaming Responses path (convertResponsesMessageToAIMessage, which sets model_name: response.model) and the Completions path.
Impact: anything attributing cost or usage by response_metadata.model_name records the alias, so per-snapshot pricing and model-version comparisons are wrong. Langfuse's LangChain callback handler ends every generation with this field, so all traced reasoning-enabled calls are recorded under the alias.
Note model_provider already sits in the merge's last-write-wins list, so this class of key is handled — model and model_name look simply missed. #11054 is the same merge behaviour surfacing on the Completions path.
Proposed fix
Write both keys from response.completed / response.incomplete instead of response.created:
} else if (event.type === "response.created") {
id = event.response.id;
response_metadata.id = event.response.id;
- response_metadata.model_name = event.response.model;
- response_metadata.model = event.response.model;
} else if (event.type === "response.completed" || event.type === "response.incomplete") {
id = event.response.id;
+ response_metadata.model_name = event.response.model;
(model is then set once by the existing Object.entries copy, so nothing concatenates.) Adding model and model_name next to model_provider in _mergeDicts' last-write-wins list would additionally fix #11054 and make this robust for every provider.
Related
System Info
@langchain/openai 1.5.10, @langchain/core 1.2.9, langchain 1.5.10 (all latest at time of filing)
- macOS (Darwin arm64)
- Node.js 26.7.0 / Bun 1.4.0
Checked other resources
Example Code
No API key needed — the converter is exported from the package root:
Actual output on
@langchain/openai1.5.10:Expected: both
gpt-5.4-2026-03-05. The non-streaming path already does this —convertResponsesMessageToAIMessageallowlistsmodel: response.modelandmodel_name: response.modelfrom the completed response.The
response.completedbranch carries the resolved snapshot, but only copies the keys present on its response object:event.responsehas nomodel_namekey, somodel_nameis never corrected.modelis written a second time — and because neithermodelnormodel_nameappears in the last-write-wins list_mergeDictsconsults (["id", "name", "output_version", "model_provider"]) nor inDEFAULT_MERGE_IGNORE_KEYS, the second write is string-appended instead of replacing:Expected: both
"gpt-5.4-2026-03-05", matching the non-streaming Responses path (convertResponsesMessageToAIMessage, which setsmodel_name: response.model) and the Completions path.Impact: anything attributing cost or usage by
response_metadata.model_namerecords the alias, so per-snapshot pricing and model-version comparisons are wrong. Langfuse's LangChain callback handler ends every generation with this field, so all traced reasoning-enabled calls are recorded under the alias.Note
model_provideralready sits in the merge's last-write-wins list, so this class of key is handled —modelandmodel_namelook simply missed. #11054 is the same merge behaviour surfacing on the Completions path.Proposed fix
Write both keys from
response.completed/response.incompleteinstead ofresponse.created:} else if (event.type === "response.created") { id = event.response.id; response_metadata.id = event.response.id; - response_metadata.model_name = event.response.model; - response_metadata.model = event.response.model; } else if (event.type === "response.completed" || event.type === "response.incomplete") { id = event.response.id; + response_metadata.model_name = event.response.model;(
modelis then set once by the existingObject.entriescopy, so nothing concatenates.) Addingmodelandmodel_namenext tomodel_providerin_mergeDicts' last-write-wins list would additionally fix #11054 and make this robust for every provider.Related
ChatOpenAI streaming duplicates response_metadata.model_name / finish_reason (and doubles response_metadata.usage) when the provider emits multiple finish_reason chunks (e.g. OpenRouter) #11054 / PR fix(openai): keep scalar response_metadata last-wins across finish_reason chunks #11084 — the same merge-concat class on the Completions path. fix(openai): keep scalar response_metadata last-wins across finish_reason chunks #11084 fixes it with a local
mergeScalarResponseMetadatainChatOpenAICompletions._streamResponseChunks, so it does not reach the Responses converter.ChatOpenRouter streaming responses do not preserve routed model metadata #11409 — same theme (streaming loses the real model) in
@langchain/openrouter.ChatOpenAI (Responses API, streaming): response_metadata embeds full request
toolsschemas on every AIMessage #11162 / PR fix(openai): stop persisting echoed request fields in streamed response_metadata #11163 — please read together with this issue. fix(openai): stop persisting echoed request fields in streamed response_metadata #11163 replaces theObject.entries(event.response)copy withObject.entries(msg.response_metadata). That allowlist contains bothmodel: response.modelandmodel_name: response.model, whileresponse.createdstill writes both keys with the requested alias — so after fix(openai): stop persisting echoed request fields in streamed response_metadata #11163 the merge concatenatesmodel_nametoo:Removing the two
response.createdwrites (below) is the prerequisite that makes fix(openai): stop persisting echoed request fields in streamed response_metadata #11163 safe.System Info
@langchain/openai1.5.10,@langchain/core1.2.9,langchain1.5.10 (all latest at time of filing)