UNPKG

@langchain/core

Version:
1 lines 18 kB
{"version":3,"file":"openai_completions_stream.cjs","names":["finalizeContentBlock"],"sources":["../../src/language_models/openai_completions_stream.ts"],"sourcesContent":["/**\n * Converts OpenAI Chat Completions-shaped stream chunks into\n * {@link ChatModelStreamEvent}s.\n *\n * Used by `@langchain/openai` and OpenAI-compatible providers (Groq, Mistral,\n * OpenRouter, IBM watsonx, etc.) without requiring a dependency on\n * `@langchain/openai`.\n *\n * @module\n */\n\nimport { finalizeContentBlock } from \"./compat.js\";\nimport type { ChatModelStreamEvent, FinishReason } from \"./event.js\";\nimport type { ContentBlock } from \"../messages/content/index.js\";\nimport type { UsageMetadata } from \"../messages/metadata.js\";\n\nexport type OpenAICompletionsFinishReason =\n | \"stop\"\n | \"length\"\n | \"max_tokens\"\n | \"tool_calls\"\n | \"function_call\"\n | \"content_filter\"\n | null;\n\nexport interface OpenAICompletionsUsage {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n prompt_tokens_details?: {\n audio_tokens?: number;\n cached_tokens?: number;\n };\n completion_tokens_details?: {\n audio_tokens?: number;\n reasoning_tokens?: number;\n };\n}\n\nexport interface OpenAICompletionsToolCallDelta {\n index?: number;\n id?: string;\n type?: \"function\";\n function?: {\n name?: string;\n arguments?: string;\n };\n}\n\nexport interface OpenAICompletionsDelta {\n role?: string;\n content?: string | null;\n reasoning_content?: string;\n reasoning?: string;\n tool_calls?: OpenAICompletionsToolCallDelta[];\n function_call?: unknown;\n audio?: {\n id?: string;\n data?: string;\n transcript?: string;\n };\n}\n\nexport interface OpenAICompletionsStreamChoice {\n index?: number;\n finish_reason?: OpenAICompletionsFinishReason;\n logprobs?: unknown;\n delta?: OpenAICompletionsDelta;\n}\n\nexport interface OpenAICompletionsStreamChunk {\n id?: string;\n model?: string;\n service_tier?: string | null;\n system_fingerprint?: string | null;\n usage?: OpenAICompletionsUsage | null;\n x_groq?: {\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n };\n };\n choices?: OpenAICompletionsStreamChoice[];\n}\n\nexport interface ConvertOpenAICompletionsStreamOptions {\n streamUsage?: boolean;\n /** Provider id for passthrough events (default `\"openai\"`). */\n provider?: string;\n /** Optional per-chunk transform before conversion. */\n mapChunk?: (\n chunk: OpenAICompletionsStreamChunk\n ) => OpenAICompletionsStreamChunk;\n}\n\ntype BlockKey = \"text\" | \"reasoning\" | \"audio\" | `tool:${number}`;\n\n/**\n * Convert an async iterable of OpenAI Chat Completions-shaped stream chunks into\n * LangChain `ChatModelStreamEvent`s with typed deltas.\n */\nexport async function* convertOpenAICompletionsStream(\n source: AsyncIterable<OpenAICompletionsStreamChunk>,\n options: ConvertOpenAICompletionsStreamOptions = {}\n): AsyncGenerator<ChatModelStreamEvent> {\n const shouldStreamUsage = options.streamUsage ?? true;\n const provider = options.provider ?? \"openai\";\n const mapChunk = options.mapChunk;\n\n const blockAccumulators = new Map<\n number,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n Record<string, any>\n >();\n const blockKeyToIndex = new Map<BlockKey, number>();\n let nextBlockIndex = 0;\n let messageStarted = false;\n let usageSnapshot: UsageMetadata | undefined;\n let finishReason: FinishReason | undefined;\n let responseMetadata: Record<string, unknown> | undefined;\n let emittedProviderMetadata = false;\n\n const getOrCreateBlockIndex = (\n key: BlockKey,\n initial: Record<string, unknown>\n ): { index: number; isNew: boolean } => {\n const existing = blockKeyToIndex.get(key);\n if (existing !== undefined) {\n return { index: existing, isNew: false };\n }\n const index = nextBlockIndex++;\n blockKeyToIndex.set(key, index);\n blockAccumulators.set(index, { ...initial });\n return { index, isNew: true };\n };\n\n for await (let data of source) {\n if (mapChunk) {\n data = mapChunk(data);\n }\n if (!messageStarted) {\n messageStarted = true;\n yield {\n event: \"message-start\" as const,\n id: data.id,\n };\n }\n\n if (!emittedProviderMetadata && (data.model || data.service_tier)) {\n emittedProviderMetadata = true;\n yield {\n event: \"provider\" as const,\n provider,\n name: \"stream_metadata\",\n payload: {\n model: data.model,\n service_tier: data.service_tier,\n },\n };\n }\n\n if (data.usage && shouldStreamUsage) {\n usageSnapshot = buildUsageSnapshot(data.usage);\n yield { event: \"usage\" as const, usage: usageSnapshot };\n }\n\n const groqUsage = data.x_groq?.usage;\n if (groqUsage && shouldStreamUsage) {\n usageSnapshot = buildGroqUsageSnapshot(groqUsage);\n yield { event: \"usage\" as const, usage: usageSnapshot };\n }\n\n const choice = data.choices?.[0];\n if (!choice) {\n continue;\n }\n\n if (choice.finish_reason != null) {\n finishReason = mapFinishReason(choice.finish_reason);\n responseMetadata = buildResponseMetadata(data, choice);\n }\n\n const { delta } = choice;\n if (!delta) {\n continue;\n }\n\n const reasoningText = getReasoningDeltaText(delta);\n if (reasoningText) {\n const key: BlockKey = \"reasoning\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"reasoning\" as const,\n reasoning: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"reasoning\", reasoning: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.reasoning = (acc.reasoning ?? \"\") + reasoningText;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"reasoning-delta\" as const,\n reasoning: reasoningText,\n },\n };\n }\n\n if (delta.content) {\n const key: BlockKey = \"text\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"text\" as const,\n text: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"text\", text: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.text = (acc.text ?? \"\") + delta.content;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"text-delta\" as const, text: delta.content },\n };\n }\n\n if (Array.isArray(delta.tool_calls)) {\n for (const rawToolCall of delta.tool_calls) {\n const toolIndex = rawToolCall.index ?? 0;\n const key: BlockKey = `tool:${toolIndex}`;\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"tool_call_chunk\" as const,\n id: rawToolCall.id,\n name: rawToolCall.function?.name,\n args: \"\",\n index: toolIndex,\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: {\n type: \"tool_call_chunk\",\n id: rawToolCall.id,\n name: rawToolCall.function?.name,\n args: \"\",\n index: toolIndex,\n } as ContentBlock,\n };\n }\n\n const acc = blockAccumulators.get(index)!;\n if (rawToolCall.id != null) acc.id = rawToolCall.id;\n if (rawToolCall.function?.name != null) {\n acc.name = rawToolCall.function.name;\n }\n const argDelta = rawToolCall.function?.arguments ?? \"\";\n acc.args = (acc.args ?? \"\") + argDelta;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"block-delta\" as const,\n fields: {\n type: \"tool_call_chunk\",\n ...(acc.id != null ? { id: acc.id } : {}),\n ...(acc.name != null ? { name: acc.name } : {}),\n args: acc.args,\n },\n },\n };\n }\n }\n\n if (delta.audio) {\n const key: BlockKey = \"audio\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"audio\" as const,\n id: delta.audio.id,\n data: \"\",\n mimeType: \"audio/pcm\",\n transcript: delta.audio.transcript ?? \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: {\n type: \"audio\",\n id: delta.audio.id,\n data: \"\",\n mimeType: \"audio/pcm\",\n transcript: delta.audio.transcript ?? \"\",\n } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n if (delta.audio.transcript) {\n acc.transcript = (acc.transcript ?? \"\") + delta.audio.transcript;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"block-delta\" as const,\n fields: {\n type: \"audio\",\n transcript: acc.transcript,\n },\n },\n };\n }\n if (delta.audio.data) {\n acc.data = (acc.data ?? \"\") + delta.audio.data;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"data-delta\" as const,\n data: delta.audio.data,\n encoding: \"base64\" as const,\n },\n };\n }\n }\n\n if (delta.function_call) {\n yield {\n event: \"provider\" as const,\n provider,\n name: \"function_call\",\n payload: delta.function_call,\n };\n }\n\n if (choice.logprobs) {\n yield {\n event: \"provider\" as const,\n provider,\n name: \"logprobs\",\n payload: choice.logprobs,\n };\n }\n }\n\n for (const [index, acc] of blockAccumulators) {\n const finalized = finalizeContentBlock(acc as ContentBlock);\n yield {\n event: \"content-block-finish\" as const,\n index,\n content: finalized,\n };\n }\n\n yield {\n event: \"message-finish\" as const,\n reason: finishReason,\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n ...(responseMetadata ? { responseMetadata } : {}),\n };\n}\n\nfunction getReasoningDeltaText(\n delta: OpenAICompletionsDelta\n): string | undefined {\n const reasoning = delta.reasoning_content ?? delta.reasoning;\n return typeof reasoning === \"string\" && reasoning.length > 0\n ? reasoning\n : undefined;\n}\n\nfunction buildGroqUsageSnapshot(usage: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n}): UsageMetadata {\n return {\n input_tokens: usage.prompt_tokens ?? 0,\n output_tokens: usage.completion_tokens ?? 0,\n total_tokens: usage.total_tokens ?? 0,\n };\n}\n\nfunction mapFinishReason(reason: OpenAICompletionsFinishReason): FinishReason {\n switch (reason) {\n case \"stop\":\n return \"stop\";\n case \"length\":\n case \"max_tokens\":\n return \"length\";\n case \"tool_calls\":\n case \"function_call\":\n return \"tool_use\";\n case \"content_filter\":\n return \"content_filter\";\n default:\n return \"stop\";\n }\n}\n\nfunction buildUsageSnapshot(usage: OpenAICompletionsUsage): UsageMetadata {\n const inputTokenDetails = {\n ...(usage.prompt_tokens_details?.audio_tokens != null && {\n audio: usage.prompt_tokens_details.audio_tokens,\n }),\n ...(usage.prompt_tokens_details?.cached_tokens != null && {\n cache_read: usage.prompt_tokens_details.cached_tokens,\n }),\n };\n const outputTokenDetails = {\n ...(usage.completion_tokens_details?.audio_tokens != null && {\n audio: usage.completion_tokens_details.audio_tokens,\n }),\n ...(usage.completion_tokens_details?.reasoning_tokens != null && {\n reasoning: usage.completion_tokens_details.reasoning_tokens,\n }),\n };\n return {\n input_tokens: usage.prompt_tokens ?? 0,\n output_tokens: usage.completion_tokens ?? 0,\n total_tokens: usage.total_tokens ?? 0,\n ...(Object.keys(inputTokenDetails).length > 0 && {\n input_token_details: inputTokenDetails,\n }),\n ...(Object.keys(outputTokenDetails).length > 0 && {\n output_token_details: outputTokenDetails,\n }),\n };\n}\n\nfunction buildResponseMetadata(\n data: OpenAICompletionsStreamChunk,\n choice: OpenAICompletionsStreamChoice\n): Record<string, unknown> {\n return {\n model_provider: \"openai\",\n model_name: data.model,\n system_fingerprint: data.system_fingerprint,\n service_tier: data.service_tier,\n finish_reason: choice.finish_reason,\n ...(data.usage ? { usage: data.usage } : {}),\n };\n}\n"],"mappings":"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"}