UNPKG

@earendil-works/pi-ai

Version:

Unified LLM API with automatic model discovery and provider configuration

770 lines • 36 kB
import { calculateCost } from "../models.js"; import { shortHash } from "../utils/hash.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { ToolCallArgumentsStream } from "../utils/streaming-json.js"; import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js"; import { resolveTranscript, resolveTranscriptTools } from "../utils/transcript.js"; import { appendGrammarToolInputJsonDelta, closeGrammarToolInputJson, getGrammarToolInput, getJsonSchemaToolParameters, resolveGrammarConstrainedSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js"; import { transformMessages } from "./transform-messages.js"; // ============================================================================= // Utilities // ============================================================================= function encodeTextSignatureV1(id, phase) { const payload = { v: 1, id }; if (phase) payload.phase = phase; return JSON.stringify(payload); } function parseTextSignature(signature) { if (!signature) return undefined; if (signature.startsWith("{")) { try { const parsed = JSON.parse(signature); if (parsed.v === 1 && typeof parsed.id === "string") { if (parsed.phase === "commentary" || parsed.phase === "final_answer") { return { id: parsed.id, phase: parsed.phase }; } return { id: parsed.id }; } } catch { // Fall through to legacy plain-string handling. } } return { id: signature }; } function convertToolResultOutput(model, content) { const textResult = content .filter((c) => c.type === "text") .map((c) => c.text) .join("\n"); const images = content.filter((c) => c.type === "image"); const hasText = textResult.trim().length > 0; if (images.length === 0 || !model.input.includes("image")) { return sanitizeSurrogates(hasText ? textResult : images.length > 0 ? "(see attached image)" : "(no tool output)"); } const output = []; if (hasText) { output.push({ type: "input_text", text: sanitizeSurrogates(textResult) }); } for (const image of images) { output.push({ type: "input_image", detail: "auto", image_url: `data:${image.mimeType};base64,${image.data}`, }); } return output; } // ============================================================================= // Message conversion // ============================================================================= /** * How a Responses transport expresses tool changes. * * `additional_tools` items accumulate: a later item adds tools and replaces same-name * definitions from earlier items, but it cannot replace a top-level tool and cannot remove any * tool (verified with strict schemas on the OpenAI, Codex, and Copilot backends, October 2026). * Like Codex, every tool is therefore declared in `additional_tools` items, starting with one * at the head of the input, and removals are announced as text. Synthetic tool search can * only add tools. Without mid-conversation system messages there is nothing to anchor. */ export function getResponsesToolSupport(options) { if (options.supportsMidConvoSystemMessages && options.supportsAdditionalTools) return "changes"; return options.supportsToolSearch ? "additions" : "none"; } /** * Precedes every additional_tools item after the first. Without it, models read the latest item as * the complete tool set once a removal was announced; Codex precedes its incremental tool batches * with the same guidance. Restating the tools that are still unavailable keeps smaller models * (gpt-5.6-luna, gpt-6-luna) from calling them again after later loads (verified October 2026). */ function renderIncrementalToolsNotice(unavailable) { const notice = "This is an incremental tools update. Previously declared tools remain available unless explicitly marked unavailable. A tool declared again here is available again, and its latest definition replaces the earlier one."; return unavailable.size > 0 ? `${notice}\nStill unavailable: ${[...unavailable].join(", ")}.` : notice; } function renderToolRemovalNotice(names) { return `The following tools are no longer available. Do not call them:\n${names.map((name) => `- ${name}`).join("\n")}`; } export function convertResponsesMessages(model, context, allowedToolCallProviders, options) { const normalizedContext = resolveTranscript(context, options?.supportsMidConvoSystemMessages); const messages = []; const normalizeIdPart = (part) => { const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_"); const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized; return normalized.replace(/_+$/, ""); }; const buildForeignResponsesItemId = (itemId) => { const normalized = `fc_${shortHash(itemId)}`; return normalized.length > 64 ? normalized.slice(0, 64) : normalized; }; const normalizeToolCallId = (id, _targetModel, source) => { if (!allowedToolCallProviders.has(model.provider)) return normalizeIdPart(id); if (!id.includes("|")) return normalizeIdPart(id); const [callId, itemId] = id.split("|"); const normalizedCallId = normalizeIdPart(callId); const isForeignToolCall = source.provider !== model.provider || source.api !== model.api; let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId); // OpenAI Responses API requires item id to start with "fc" if (!normalizedItemId.startsWith("fc_")) { normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`); } return `${normalizedCallId}|${normalizedItemId}`; }; const transformedMessages = transformMessages(normalizedContext.messages, model, normalizeToolCallId); const toolSupport = getResponsesToolSupport(options ?? {}); const transcriptTools = resolveTranscriptTools(normalizedContext.messages, toolSupport); let declaredAdditionalTools = false; /** Tools announced as removed and not declared again since. */ const unavailableTools = new Set(); const appendSystemToolAdditions = (message, seed) => { const tools = message.toolsAdded ?? []; for (const tool of tools) unavailableTools.delete(tool.name); if (tools.length === 0) return; if (toolSupport === "changes") { if (declaredAdditionalTools) { messages.push({ role: instructionRole, content: renderIncrementalToolsNotice(unavailableTools) }); } declaredAdditionalTools = true; messages.push({ type: "additional_tools", role: "developer", tools: convertResponsesTools(tools, options?.toolOptions), }); return; } if (toolSupport !== "additions") return; const names = tools.map((tool) => tool.name); const callId = `pi_tool_load_${shortHash(`${seed}:${names.join(",")}`)}`; messages.push({ type: "tool_search_call", call_id: callId, execution: "client", status: "completed", arguments: { query: names.join(" "), limit: names.length }, }); messages.push({ type: "tool_search_output", call_id: callId, execution: "client", status: "completed", tools: convertResponsesTools(tools, { ...options?.toolOptions, toolSearchResult: true }), }); }; const includeInitialSystemMessage = options?.includeSystemPrompt ?? true; const pushConfigurationUpdate = (effort) => { messages.push({ type: "configuration_update", reasoning: { effort: effort }, }); }; const compat = model.compat; const instructionRole = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system"; let msgIndex = 0; let sourceIndex = 0; let systemIndex = 0; for (const msg of transformedMessages) { const isLeadingSystemMessage = sourceIndex++ === 0 && msg.role === "system"; if (msg.role === "system") { // transformMessages keeps system messages in order, so their position matches the transcript. const anchorsToolChanges = systemIndex++ >= transcriptTools.foldedSystemMessages; if (anchorsToolChanges) appendSystemToolAdditions(msg, `system:${msgIndex}`); if (!isLeadingSystemMessage || includeInitialSystemMessage) { const parts = [isLeadingSystemMessage ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg)]; if (anchorsToolChanges) { // A redefinition is a removal plus an addition of the same name; only true removals are announced. const added = new Set((msg.toolsAdded ?? []).map((tool) => tool.name)); const removed = (msg.toolsRemoved ?? []).map((tool) => tool.name).filter((name) => !added.has(name)); if (removed.length > 0) parts.push(renderToolRemovalNotice(removed)); // Recorded after this message's additions, whose notice precedes this removal notice. for (const name of removed) unavailableTools.add(name); } const text = parts.filter((part) => part.length > 0).join("\n\n"); if (text.length > 0) { messages.push({ role: instructionRole, content: sanitizeSurrogates(text) }); } } } else if (msg.role === "user") { if (typeof msg.content === "string") { messages.push({ role: "user", content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }], }); } else { const content = msg.content.map((item) => { if (item.type === "text") { return { type: "input_text", text: sanitizeSurrogates(item.text), }; } return { type: "input_image", detail: "auto", image_url: `data:${item.mimeType};base64,${item.data}`, }; }); if (content.length === 0) continue; messages.push({ role: "user", content, }); } } else if (msg.role === "assistant") { const output = []; const assistantMsg = msg; const isSameProviderAndApi = assistantMsg.provider === model.provider && assistantMsg.api === model.api; const isSameModel = isSameProviderAndApi && assistantMsg.model === model.id; const isDifferentModel = isSameProviderAndApi && assistantMsg.model !== model.id; let textBlockIndex = 0; for (const block of msg.content) { if (block.type === "thinking") { if (block.thinkingSignature) { const reasoningItem = JSON.parse(block.thinkingSignature); output.push(reasoningItem); } } else if (block.type === "text") { const textBlock = block; const parsedSignature = parseTextSignature(textBlock.textSignature); const fallbackMessageId = textBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`; textBlockIndex++; // OpenAI requires id to be max 64 characters let msgId = parsedSignature?.id; if (!msgId) { msgId = fallbackMessageId; } else if (msgId.length > 64) { msgId = `msg_${shortHash(msgId)}`; } output.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: sanitizeSurrogates(textBlock.text), annotations: [] }], status: "completed", id: msgId, phase: parsedSignature?.phase, }); } else if (block.type === "toolCall") { const toolCall = block; const [callId, itemIdRaw] = toolCall.id.split("|"); const customInputProperty = options?.grammarToolInputProperties?.get(toolCall.name); let itemId = itemIdRaw; // For different-model messages, set id to undefined to avoid pairing validation. // OpenAI tracks which item IDs were paired with rs_xxx reasoning items. // By omitting the id, we avoid triggering that validation (like cross-provider does). // Also drop ids that do not match the replayed item type: function_call ids must be fc_* // and custom_tool_call ids must be ctc_*. Foreign tool call ids are normalized to fc_*, and // a call can switch between the two types when grammar tool support differs. const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_"; if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) { itemId = undefined; } if (customInputProperty !== undefined) { output.push({ type: "custom_tool_call", id: itemId, call_id: callId, name: toolCall.name, input: sanitizeSurrogates(getGrammarToolInput(toolCall.name, toolCall.arguments, customInputProperty)), ...(isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}), }); } else { output.push({ type: "function_call", id: itemId, call_id: callId, name: toolCall.name, arguments: JSON.stringify(toolCall.arguments), ...(isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}), }); } } } if (output.length === 0) continue; if (options?.configurationUpdateEffort !== undefined && isSameModel && msg.providerThinkingLevel !== undefined) { pushConfigurationUpdate(msg.providerThinkingLevel); } messages.push(...output); } else if (msg.role === "toolResult") { const [callId] = msg.toolCallId.split("|"); const output = convertToolResultOutput(model, msg.content); if (options?.grammarToolInputProperties?.has(msg.toolName)) { messages.push({ type: "custom_tool_call_output", call_id: callId, output, }); } else { messages.push({ type: "function_call_output", call_id: callId, output, }); } } if (!isLeadingSystemMessage) msgIndex++; } if (options?.configurationUpdateEffort !== undefined) pushConfigurationUpdate(options.configurationUpdateEffort); return messages; } // ============================================================================= // Tool conversion // ============================================================================= export function convertResponsesTools(tools, options) { const defaultStrict = options?.strict === undefined ? false : options.strict; const supportsStrictMode = options?.supportsStrictMode ?? true; const supportsOpenAIGrammarTools = options?.supportsOpenAIGrammarTools ?? false; return tools.map((tool) => { const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools); if (grammar) { return { type: "custom", name: tool.name, description: tool.description, format: { type: "grammar", syntax: grammar.format, definition: grammar.definition, }, ...(options?.toolSearchResult ? { defer_loading: true } : {}), }; } const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode); const strict = constrainedStrict ?? defaultStrict; const functionTool = { type: "function", name: tool.name, description: tool.description, parameters: getJsonSchemaToolParameters(tool, strict === true), ...(options?.toolSearchResult ? { defer_loading: true } : {}), }; if (supportsStrictMode) { functionTool.strict = strict; } return functionTool; }); } export async function processResponsesStream(openaiStream, output, stream, model, options) { let sawTerminalResponseEvent = false; const outputSlots = new Map(); // Tool calls whose output_item.done has not arrived yet. const unfinishedToolCalls = new Set(); const reasoningBlocksById = new Map(); const applyMessagePhaseStopReason = (item) => { if (item.type === "message" && item.phase === "final_answer") { output.stopReason = "stop"; } }; const getSlot = (outputIndex, type) => { const slot = outputSlots.get(outputIndex); return slot?.type === type ? slot : undefined; }; const pushToolCallDelta = (slot, delta) => { if (delta === undefined) return; stream.push({ type: "toolcall_delta", contentIndex: slot.contentIndex, delta, partial: output, }); }; const createSlot = (outputIndex, item) => { if (item.type === "reasoning") { const block = { type: "thinking", thinking: "" }; output.content.push(block); const slot = { type: "thinking", block, contentIndex: output.content.length - 1, }; outputSlots.set(outputIndex, slot); stream.push({ type: "thinking_start", contentIndex: slot.contentIndex, partial: output }); return slot; } if (item.type === "message") { applyMessagePhaseStopReason(item); const block = { type: "text", text: "" }; output.content.push(block); const slot = { type: "text", block, contentIndex: output.content.length - 1 }; outputSlots.set(outputIndex, slot); stream.push({ type: "text_start", contentIndex: slot.contentIndex, partial: output }); return slot; } if (item.type === "function_call") { const args = new ToolCallArgumentsStream(); const block = { type: "toolCall", id: `${item.call_id}|${item.id}`, name: item.name, arguments: item.arguments ? args.append(item.arguments) : {}, ...(item.namespace !== undefined ? { namespace: item.namespace } : {}), }; output.content.push(block); unfinishedToolCalls.add(block); const slot = { type: "toolCall", block, contentIndex: output.content.length - 1, input: { kind: "json", arguments: args }, }; outputSlots.set(outputIndex, slot); stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output }); return slot; } if (item.type === "custom_tool_call") { const inputProperty = options?.grammarToolInputProperties?.get(item.name) ?? "input"; const input = item.input || ""; const block = { type: "toolCall", id: `${item.call_id}|${item.id}`, name: item.name, arguments: { [inputProperty]: input }, ...(item.namespace !== undefined ? { namespace: item.namespace } : {}), }; output.content.push(block); unfinishedToolCalls.add(block); const slot = { type: "toolCall", block, contentIndex: output.content.length - 1, input: customInput(inputProperty, input), }; outputSlots.set(outputIndex, slot); stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output }); return slot; } return undefined; }; const getOrCreateSlot = (outputIndex, item) => { return outputSlots.get(outputIndex) ?? createSlot(outputIndex, item); }; // Azure OpenAI can omit reasoning.encrypted_content from response.output_item.done // and provide it only in response.completed.response.output. Backfill the // persisted reasoning signature from the terminal response to keep store:false // multi-turn replay stateless. See https://github.com/earendil-works/pi/issues/6409. const backfillReasoningSignatures = (responseOutput) => { for (const item of responseOutput) { if (item.type !== "reasoning" || !item.encrypted_content) continue; const block = reasoningBlocksById.get(item.id); if (!block?.thinkingSignature) continue; const storedItem = JSON.parse(block.thinkingSignature); if (storedItem.encrypted_content) continue; block.thinkingSignature = JSON.stringify({ ...storedItem, encrypted_content: item.encrypted_content, }); } }; const finalizeResponse = (response) => { sawTerminalResponseEvent = true; backfillReasoningSignatures(response.output ?? []); if (response?.id) { output.responseId = response.id; } if (response?.usage) { const inputDetails = response.usage.input_tokens_details; const cachedTokens = inputDetails?.cached_tokens || 0; const cacheWriteTokens = inputDetails?.cache_write_tokens || 0; output.usage = { // OpenAI includes cached and cache-write tokens in input_tokens, so subtract both. input: Math.max(0, (response.usage.input_tokens || 0) - cachedTokens - cacheWriteTokens), output: response.usage.output_tokens || 0, cacheRead: cachedTokens, cacheWrite: cacheWriteTokens, reasoning: response.usage.output_tokens_details?.reasoning_tokens || 0, totalTokens: response.usage.total_tokens || 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }; } calculateCost(model, output.usage); if (options?.applyServiceTierPricing) { const serviceTier = options.resolveServiceTier ? options.resolveServiceTier(response?.service_tier, options.serviceTier) : (response?.service_tier ?? options.serviceTier); options.applyServiceTierPricing(output.usage, serviceTier); } // Map status to stop reason. For incomplete responses, retain the provider's // specific reason so max-output truncation and content filtering stay distinct. const status = response?.status; const incompleteDetails = response?.incomplete_details; const incompleteReason = typeof incompleteDetails?.reason === "string" ? incompleteDetails.reason : undefined; output.rawStopReason = incompleteReason ? `${status}.${incompleteReason}` : status; const mappedStop = mapStopReason(status, incompleteReason); output.stopReason = mappedStop.stopReason; if (mappedStop.errorMessage === undefined) delete output.errorMessage; else output.errorMessage = mappedStop.errorMessage; if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") { output.stopReason = "toolUse"; } }; for await (const event of openaiStream) { await options?.onProviderStreamEvent?.(event, model); if (event.type === "response.created") { output.responseId = event.response.id; } else if (event.type === "response.output_item.added") { createSlot(event.output_index, event.item); } else if (event.type === "response.reasoning_summary_text.delta") { const slot = getSlot(event.output_index, "thinking"); if (!slot) continue; slot.block.thinking += event.delta; stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output, }); } else if (event.type === "response.reasoning_summary_part.done") { const slot = getSlot(event.output_index, "thinking"); if (!slot) continue; slot.block.thinking += "\n\n"; stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: "\n\n", partial: output, }); } else if (event.type === "response.reasoning_text.delta") { const slot = getSlot(event.output_index, "thinking"); if (!slot) continue; slot.block.thinking += event.delta; stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output, }); } else if (event.type === "response.output_text.delta") { const slot = getSlot(event.output_index, "text"); if (!slot) continue; slot.block.text += event.delta; stream.push({ type: "text_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output, }); } else if (event.type === "response.refusal.delta") { const slot = getSlot(event.output_index, "text"); if (!slot) continue; slot.block.text += event.delta; stream.push({ type: "text_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output, }); } else if (event.type === "response.function_call_arguments.delta") { const slot = getSlot(event.output_index, "toolCall"); if (slot?.input.kind !== "json") continue; slot.block.arguments = slot.input.arguments.append(event.delta); pushToolCallDelta(slot, event.delta); } else if (event.type === "response.function_call_arguments.done") { const slot = getSlot(event.output_index, "toolCall"); if (slot?.input.kind !== "json") continue; const replaced = slot.input.arguments.replace(event.arguments); slot.block.arguments = replaced.arguments; if (replaced.delta) pushToolCallDelta(slot, replaced.delta); } else if (event.type === "response.custom_tool_call_input.delta") { const slot = getSlot(event.output_index, "toolCall"); if (slot?.input.kind !== "custom") continue; pushToolCallDelta(slot, appendCustomInput(slot.block, slot.input, event.delta)); } else if (event.type === "response.custom_tool_call_input.done") { const slot = getSlot(event.output_index, "toolCall"); if (slot?.input.kind !== "custom") continue; pushToolCallDelta(slot, closeCustomInput(slot.block, slot.input, event.input)); } else if (event.type === "response.output_item.done") { const item = event.item; applyMessagePhaseStopReason(item); const slot = getOrCreateSlot(event.output_index, item); if (item.type === "reasoning" && slot?.type === "thinking") { const summaryText = item.summary?.map((s) => s.text).join("\n\n") || ""; const contentText = item.content?.map((c) => c.text).join("\n\n") || ""; slot.block.thinking = summaryText || contentText || slot.block.thinking; slot.block.thinkingSignature = JSON.stringify(item); reasoningBlocksById.set(item.id, slot.block); stream.push({ type: "thinking_end", contentIndex: slot.contentIndex, content: slot.block.thinking, partial: output, }); outputSlots.delete(event.output_index); } else if (item.type === "message" && slot?.type === "text") { slot.block.text = item.content?.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("") || ""; slot.block.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined); stream.push({ type: "text_end", contentIndex: slot.contentIndex, content: slot.block.text, partial: output, }); outputSlots.delete(event.output_index); } else if (item.type === "function_call" && slot?.type === "toolCall" && slot.input.kind === "json") { if (item.arguments) slot.input.arguments.replace(item.arguments); Object.assign(slot.block, slot.input.arguments.finish()); if (item.namespace !== undefined) slot.block.namespace = item.namespace; unfinishedToolCalls.delete(slot.block); stream.push({ type: "toolcall_end", contentIndex: slot.contentIndex, toolCall: slot.block, partial: output, }); outputSlots.delete(event.output_index); } else if (item.type === "custom_tool_call" && slot?.type === "toolCall" && slot.input.kind === "custom") { pushToolCallDelta(slot, closeCustomInput(slot.block, slot.input, item.input)); if (item.namespace !== undefined) slot.block.namespace = item.namespace; unfinishedToolCalls.delete(slot.block); stream.push({ type: "toolcall_end", contentIndex: slot.contentIndex, toolCall: slot.block, partial: output, }); outputSlots.delete(event.output_index); } } else if (event.type === "response.completed" || event.type === "response.incomplete") { finalizeResponse(event.response); } else if (event.type === "error") { throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error"); } else if (event.type === "response.failed") { sawTerminalResponseEvent = true; output.rawStopReason = event.response?.status; const error = event.response?.error; const details = event.response?.incomplete_details; const msg = error ? `${error.code || "unknown"}: ${error.message || "no message"}` : details?.reason ? `incomplete: ${details.reason}` : "Unknown error (no error details in response)"; throw new Error(msg); } } if (!sawTerminalResponseEvent) { throw new Error("OpenAI Responses stream ended before a terminal response event"); } // The agent runs every tool call in the final message. Refuse to hand over calls whose // output_item.done never arrived: their arguments may be cut off or mixed up, e.g. when a // non-compliant server omits output_index. if (output.stopReason === "toolUse") { for (const toolCall of unfinishedToolCalls) { throw new Error(`OpenAI Responses stream completed with an unfinished tool call: ${toolCall.name} (${toolCall.id})`); } } } /** Stream state of a custom tool call that starts with `initialInput`. */ function customInput(property, initialInput) { const buffer = { input: "", started: false, closed: false }; return { kind: "custom", property, buffer, unsent: appendGrammarToolInputJsonDelta(buffer, property, initialInput) ?? "", }; } /** Append to a streaming custom tool input; returns the matching piece of its JSON arguments text. */ function appendCustomInput(block, input, delta) { return sendCustomInput(block, input, appendGrammarToolInputJsonDelta(input.buffer, input.property, delta)); } /** Close a custom tool input with the provider's complete input, if it sent one. */ function closeCustomInput(block, input, finalInput) { return sendCustomInput(block, input, closeGrammarToolInputJson(input.buffer, input.property, finalInput)); } function sendCustomInput(block, input, jsonDelta) { block.arguments = { [input.property]: input.buffer.input }; if (input.unsent === "") return jsonDelta; const sent = input.unsent + (jsonDelta ?? ""); input.unsent = ""; return sent; } function mapStopReason(status, incompleteReason) { if (!status) return { stopReason: "stop" }; switch (status) { case "completed": return { stopReason: "stop" }; case "incomplete": if (incompleteReason === "max_output_tokens") { return { stopReason: "length" }; } return { stopReason: "error", errorMessage: incompleteReason ? `Response incomplete: ${incompleteReason}` : "Response incomplete without a provider reason", }; case "failed": case "cancelled": return { stopReason: "error" }; // These two are wonky ... case "in_progress": case "queued": return { stopReason: "stop" }; default: { const _exhaustive = status; throw new Error(`Unhandled stop reason: ${_exhaustive}`); } } } //# sourceMappingURL=openai-responses-shared.js.map