@langchain/core
Version:
Core LangChain.js abstractions and schemas
404 lines (403 loc) • 12.1 kB
JavaScript
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
import { AIMessageChunk } from "../messages/ai.js";
//#region src/language_models/compat.ts
/**
* Compatibility bridge: converts legacy `_streamResponseChunks`
* (`ChatGenerationChunk` / `AIMessageChunk`) output to the new
* `ChatModelStreamEvent` protocol.
*
* @module
*/
var compat_exports = /* @__PURE__ */ __exportAll({
convertChunksToEvents: () => convertChunksToEvents,
finalizeContentBlock: () => finalizeContentBlock
});
const MIME_TYPE_BY_AUDIO_FORMAT = {
wav: "audio/wav",
mp3: "audio/mpeg",
flac: "audio/flac",
opus: "audio/opus",
aac: "audio/aac",
pcm16: "audio/pcm"
};
const MIME_TYPE_BY_IMAGE_FORMAT = {
png: "image/png",
jpeg: "image/jpeg",
jpg: "image/jpeg",
webp: "image/webp",
gif: "image/gif"
};
function nextBlockIndex(activeBlocks) {
let next = 0;
for (const index of activeBlocks.keys()) if (index >= next) next = index + 1;
return next;
}
function getAdditionalKwargs(message) {
const additional = message.additional_kwargs;
return additional != null && typeof additional === "object" ? additional : {};
}
function extractImageBlocksFromToolOutputs(message) {
const toolOutputs = getAdditionalKwargs(message).tool_outputs;
if (!Array.isArray(toolOutputs)) return [];
const blocks = [];
for (const entry of toolOutputs) {
if (entry == null || typeof entry !== "object") continue;
const record = entry;
if (record.type !== "image_generation_call") continue;
const data = typeof record.result === "string" ? record.result : void 0;
const url = typeof record.url === "string" ? record.url : void 0;
if (data == null && url == null) continue;
const outputFormat = typeof record.output_format === "string" ? record.output_format.toLowerCase() : void 0;
const mimeType = (outputFormat != null ? MIME_TYPE_BY_IMAGE_FORMAT[outputFormat] : void 0) ?? "image/png";
blocks.push({
type: "image",
...typeof record.id === "string" ? { id: record.id } : {},
...url != null ? { url } : {},
...data != null ? { data } : {},
mimeType
});
}
return blocks;
}
/**
* Get the audio payload from the message.
*
* This handles the OpenAI-shaped `additional_kwargs.audio` payload used by
* legacy chunk streams; other providers must normalize into this shape first.
*
* @param message - The message to get the audio payload from.
* @returns The audio payload.
* @internal
*/
function getAudioPayload(message) {
const audio = getAdditionalKwargs(message).audio;
if (audio == null || typeof audio !== "object") return void 0;
const record = audio;
const data = typeof record.data === "string" ? record.data : void 0;
const url = typeof record.url === "string" ? record.url : void 0;
const transcript = typeof record.transcript === "string" ? record.transcript : void 0;
if (data == null && url == null && transcript == null) return void 0;
const explicitMimeType = typeof record.mime_type === "string" ? record.mime_type : typeof record.mimeType === "string" ? record.mimeType : void 0;
const format = typeof record.format === "string" ? record.format.toLowerCase() : void 0;
const mimeType = explicitMimeType ?? (format != null ? MIME_TYPE_BY_AUDIO_FORMAT[format] : void 0) ?? (data != null ? "audio/wav" : "audio/pcm");
return {
...typeof record.id === "string" ? { id: record.id } : {},
...data != null ? { data } : {},
...url != null ? { url } : {},
...transcript != null ? { transcript } : {},
mimeType
};
}
/**
* Convert an async iterable of legacy `ChatGenerationChunk`s into
* `ChatModelStreamEvent`s with typed deltas.
*/
async function* convertChunksToEvents(chunks, options) {
const activeBlocks = /* @__PURE__ */ new Map();
let messageStarted = false;
let lastUsage;
let audioStream;
const emittedImageKeys = /* @__PURE__ */ new Set();
for await (const chunk of chunks) {
options?.signal?.throwIfAborted();
const msg = chunk.message;
let usageHandledInStart = false;
if (!messageStarted) {
messageStarted = true;
const startEvent = {
event: "message-start",
id: msg.id ?? void 0
};
if (AIMessageChunk.isInstance(msg) && msg.usage_metadata) {
startEvent.usage = msg.usage_metadata;
lastUsage = { ...msg.usage_metadata };
usageHandledInStart = true;
}
yield startEvent;
}
const content = msg.content;
if (typeof content === "string") {
if (content !== "") {
const blockIndex = 0;
if (!activeBlocks.has(blockIndex)) {
const initial = {
type: "text",
text: ""
};
activeBlocks.set(blockIndex, {
type: "text",
accumulated: initial
});
yield {
event: "content-block-start",
index: blockIndex,
content: initial
};
}
const block = activeBlocks.get(blockIndex);
block.accumulated = {
...block.accumulated,
text: (block.accumulated.text ?? "") + content
};
yield {
event: "content-block-delta",
index: blockIndex,
delta: {
type: "text-delta",
text: content
}
};
}
} else if (Array.isArray(content)) for (const part of content) {
const blockIndex = typeof part.index === "number" ? part.index : activeBlocks.size;
if (!activeBlocks.has(blockIndex)) {
activeBlocks.set(blockIndex, {
type: part.type,
accumulated: { ...part }
});
yield {
event: "content-block-start",
index: blockIndex,
content: { ...part }
};
} else {
const block = activeBlocks.get(blockIndex);
const delta = contentBlockToDelta(part);
block.accumulated = applyDeltaToBlock(block.accumulated, delta);
yield {
event: "content-block-delta",
index: blockIndex,
delta
};
}
}
if (AIMessageChunk.isInstance(msg) && msg.tool_call_chunks && msg.tool_call_chunks.length > 0) for (const toolChunk of msg.tool_call_chunks) {
const blockIndex = typeof toolChunk.index === "number" ? toolChunk.index : activeBlocks.size;
if (!activeBlocks.has(blockIndex)) {
const initial = {
type: "tool_call_chunk",
id: toolChunk.id,
name: toolChunk.name,
args: "",
index: blockIndex
};
activeBlocks.set(blockIndex, {
type: "tool_call_chunk",
accumulated: initial
});
yield {
event: "content-block-start",
index: blockIndex,
content: initial
};
}
const acc = activeBlocks.get(blockIndex).accumulated;
if (toolChunk.id != null) acc.id = toolChunk.id;
if (toolChunk.name != null) acc.name = toolChunk.name;
acc.args = (acc.args ?? "") + (toolChunk.args ?? "");
yield {
event: "content-block-delta",
index: blockIndex,
delta: {
type: "block-delta",
fields: {
type: "tool_call_chunk",
..."id" in acc && acc.id != null ? { id: acc.id } : {},
..."name" in acc && acc.name != null ? { name: acc.name } : {},
args: acc.args
}
}
};
}
const audioPayload = getAudioPayload(msg);
if (audioPayload != null) {
if (audioStream == null) {
const index = nextBlockIndex(activeBlocks);
audioStream = {
index,
id: audioPayload.id,
mimeType: audioPayload.mimeType,
transcript: ""
};
const initial = {
type: "audio",
...audioPayload.id != null ? { id: audioPayload.id } : {},
...audioPayload.url != null ? { url: audioPayload.url } : {},
data: "",
mimeType: audioPayload.mimeType
};
activeBlocks.set(index, {
type: "audio",
accumulated: initial
});
yield {
event: "content-block-start",
index,
content: initial
};
}
const activeAudio = activeBlocks.get(audioStream.index);
if (activeAudio != null) {
const accumulated = activeAudio.accumulated;
if (audioPayload.id != null && audioStream.id == null) {
audioStream.id = audioPayload.id;
accumulated.id = audioPayload.id;
}
if (audioPayload.transcript != null) {
audioStream.transcript += audioPayload.transcript;
accumulated.transcript = audioStream.transcript;
yield {
event: "content-block-delta",
index: audioStream.index,
delta: {
type: "block-delta",
fields: {
type: "audio",
transcript: audioStream.transcript
}
}
};
}
if (audioPayload.data != null && audioPayload.data.length > 0) {
accumulated.data = (accumulated.data ?? "") + audioPayload.data;
yield {
event: "content-block-delta",
index: audioStream.index,
delta: {
type: "data-delta",
data: audioPayload.data,
encoding: "base64"
}
};
}
}
}
for (const imageBlock of extractImageBlocksFromToolOutputs(msg)) {
const imageRecord = imageBlock;
const imageKey = imageRecord.id ?? imageRecord.url ?? (imageRecord.data != null ? `${imageRecord.data.length}:${imageRecord.data.slice(0, 32)}` : void 0);
if (imageKey != null && emittedImageKeys.has(imageKey)) continue;
if (imageKey != null) emittedImageKeys.add(imageKey);
const index = nextBlockIndex(activeBlocks);
activeBlocks.set(index, {
type: "image",
accumulated: imageBlock
});
yield {
event: "content-block-start",
index,
content: imageBlock
};
}
if (!usageHandledInStart && AIMessageChunk.isInstance(msg) && msg.usage_metadata) {
const chunkUsage = msg.usage_metadata;
if (!lastUsage) lastUsage = { ...chunkUsage };
else lastUsage = {
input_tokens: lastUsage.input_tokens + chunkUsage.input_tokens,
output_tokens: lastUsage.output_tokens + chunkUsage.output_tokens,
total_tokens: lastUsage.total_tokens + chunkUsage.total_tokens
};
yield {
event: "usage",
usage: { ...lastUsage }
};
}
}
for (const [index, block] of activeBlocks) yield {
event: "content-block-finish",
index,
content: finalizeContentBlock(block.accumulated)
};
yield {
event: "message-finish",
reason: "stop",
...lastUsage ? { usage: lastUsage } : {}
};
}
/**
* Apply a typed delta to an accumulated content block.
* @internal
*/
function applyDeltaToBlock(block, delta) {
switch (delta.type) {
case "text-delta":
if (block.type === "text") return {
...block,
text: (block.text ?? "") + delta.text
};
return block;
case "reasoning-delta":
if (block.type === "thinking") return {
...block,
thinking: (block.thinking ?? "") + delta.reasoning
};
if (block.type === "reasoning") return {
...block,
reasoning: (block.reasoning ?? "") + delta.reasoning
};
return block;
case "data-delta": return {
...block,
data: (block.data ?? "") + delta.data
};
case "block-delta": return {
...block,
...delta.fields
};
default: throw new Error(`Unknown delta type: ${JSON.stringify(delta)}`);
}
}
function contentBlockToDelta(block) {
if (block.type === "text") return {
type: "text-delta",
text: block.text
};
if (block.type === "reasoning") return {
type: "reasoning-delta",
reasoning: block.reasoning
};
if (block.type === "thinking" && typeof block.thinking === "string") return {
type: "reasoning-delta",
reasoning: block.thinking
};
if (typeof block.data === "string") return {
type: "data-delta",
data: block.data,
encoding: "base64"
};
if (typeof block.type === "string") return {
type: "block-delta",
fields: { ...block }
};
throw new Error(`Unsupported content block delta: ${JSON.stringify(block)}`);
}
/**
* Finalize a content block for the finish event.
* For tool calls, parse the accumulated JSON args string.
*/
function finalizeContentBlock(block) {
if (block.type === "tool_call_chunk") {
const chunk = block;
let parsedArgs;
try {
parsedArgs = JSON.parse(chunk.args ?? "{}");
} catch {
return {
type: "invalid_tool_call",
id: chunk.id,
name: chunk.name,
args: chunk.args,
error: "Failed to parse tool call arguments as JSON"
};
}
return {
type: "tool_call",
id: chunk.id,
name: chunk.name,
args: parsedArgs
};
}
return block;
}
//#endregion
export { compat_exports, convertChunksToEvents, finalizeContentBlock };
//# sourceMappingURL=compat.js.map