import { getSystemMessageText, renderSystemMessageUpdate, resolveTranscript, resolveTranscriptTools, type Api, type Message, type Model, type SystemMessage, type Tool, type TranscriptContext, type Usage, } from "@earendil-works/pi-ai"; import type { ResponseInput as OpenAIResponseInput, ResponseOutputMessage, ResponseInputItem, ResponseToolSearchOutputItemParam, Tool as OpenAITool, } from "openai/resources/responses/responses.js"; import { getJsonSchemaToolParameters, getGrammarToolInput, resolveGrammarConstrainedSampling, resolveJsonSchemaStrictSampling, } from "../constrained-sampling.js"; import { parseTextSignature, shortHash, type TextSignaturePhase } from "./signatures.ts"; import { normalizeResponsesToolHistory } from "./tool-history.ts"; import { normalizeResponsesMessageHistory } from "./message-history.ts"; import { encryptedToolOutputFromDetails, opaqueToolOutputsFromDetails, imageDetailForResponses, isImageGenerationCallBlock, isWebSearchCallBlock, sanitizeImageGenerationCallItem, sanitizeWebSearchCallItem, recordedCustomInputProperty, recordedResponsesNamespace, type ImageDetail, type ImageGenerationCallBlock, type WebSearchCallBlock } from "./native-items.ts"; import { unrouteContextNamespaceToolCall } from "../../context-management/namespace-tools.ts"; import type { ServiceTier } from "../openai-codex/types.ts"; type InternalAssistantContent = Extract["content"][number] | ImageGenerationCallBlock | WebSearchCallBlock; type ImageContentWithDetail = { type: "image"; data: string; mimeType: string; detail?: ImageDetail | undefined }; // Codex supports partial_answer before the OpenAI SDK's Responses declarations. type ResponseInput = Array | (Omit & { phase?: TextSignaturePhase | null })>; export interface OpenAIResponsesStreamOptions { serviceTier?: ServiceTier; grammarToolInputProperties?: ReadonlyMap | undefined; resolveServiceTier?: ( responseServiceTier: ServiceTier, requestServiceTier: ServiceTier, ) => ServiceTier; applyServiceTierPricing?: (usage: Usage, serviceTier: ServiceTier) => void; onOutputItemDone?: (item: unknown) => void; } interface ConvertResponsesMessagesOptions { includeSystemPrompt?: boolean | undefined; grammarToolInputProperties?: ReadonlyMap | undefined; supportsMidConvoSystemMessages?: boolean | undefined; supportsAdditionalTools?: boolean | undefined; supportsToolSearch?: boolean | undefined; toolOptions?: ConvertResponsesToolsOptions | undefined; } interface ConvertResponsesToolsOptions { strict?: boolean | null | undefined; supportsStrictMode?: boolean | undefined; supportsOpenAIGrammarTools?: boolean | undefined; toolSearchResult?: boolean | undefined; } export const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]); export function normalizeResponsesId(part: string): string { const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_"); const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized; return normalized.replace(/_+$/, ""); } function sanitizeSurrogates(text: string): string { return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?( model: Model, context: TranscriptContext, allowedToolCallProviders: ReadonlySet, options?: ConvertResponsesMessagesOptions, ): ResponseInput { const normalizedContext = resolveTranscript(context, options?.supportsMidConvoSystemMessages); const messages: ResponseInput = []; const recordedCustomInputs = new Map(normalizedContext.messages.flatMap(message => message.role === "assistant" ? message.content.flatMap(block => { if (block.type !== "toolCall") return []; const property = recordedCustomInputProperty(block, message.api); return property === undefined ? [] : [[block.id, property] as const]; }) : [])); const customCallIds = new Set(); const buildForeignResponsesItemId = (itemId: string, prefix: "fc" | "ctc") => { const normalized = `${prefix}_${shortHash(itemId)}`; return normalized.length > 64 ? normalized.slice(0, 64) : normalized; }; const normalizeToolCallId = (id: string, _targetModel: Model, source: Extract) => { const property = recordedCustomInputs.get(id); const remember = (normalized: string) => { if (property !== undefined) recordedCustomInputs.set(normalized, property); return normalized; }; if (!allowedToolCallProviders.has(model.provider)) return remember(normalizeResponsesId(id)); if (!id.includes("|")) return remember(normalizeResponsesId(id)); const [callId, itemId] = id.split("|") as [string, string | undefined]; const normalizedCallId = normalizeResponsesId(callId); const isForeignToolCall = source.provider !== model.provider || source.api !== model.api; const prefix = property === undefined ? "fc" : "ctc"; let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId ?? "", prefix) : normalizeResponsesId(itemId ?? ""); if (!normalizedItemId.startsWith(prefix + "_")) normalizedItemId = normalizeResponsesId(`${prefix}_${normalizedItemId}`); return remember(`${normalizedCallId}|${normalizedItemId}`); }; const transformedMessages = normalizeResponsesMessageHistory(normalizedContext.messages, model as Model, normalizeToolCallId as never); const transcriptTools = resolveTranscriptTools( normalizedContext.messages, options?.supportsAdditionalTools || options?.supportsToolSearch ? "additions" : "none", ); const anchoredToolNames = new Set(); let declarationIndex = 0; for (const message of normalizedContext.messages) { if (message.role !== "system") continue; if (declarationIndex++ < transcriptTools.foldedSystemMessages) continue; for (const tool of message.toolsAdded ?? []) anchoredToolNames.add(tool.name); } const appendSystemToolAdditions = (message: SystemMessage, seed: string): void => { const tools = message.toolsAdded ?? []; if (tools.length === 0) return; if (options?.supportsAdditionalTools) { messages.push({ type: "additional_tools", role: "developer", tools: convertResponsesTools(tools, options.toolOptions), } satisfies ResponseInputItem); return; } if (!options?.supportsToolSearch) return; const names = tools.map((tool) => tool.name); const searchCallId = `pi_tool_load_${shortHash(`${seed}:${names.join(",")}`)}`; messages.push({ type: "tool_search_call", call_id: searchCallId, execution: "client", status: "completed", arguments: { query: names.join(" "), limit: names.length }, } satisfies ResponseInputItem); messages.push({ type: "tool_search_output", call_id: searchCallId, execution: "client", status: "completed", tools: convertResponsesTools(tools, { ...options.toolOptions, toolSearchResult: true }), } satisfies ResponseToolSearchOutputItemParam); }; const includeSystemPrompt = options?.includeSystemPrompt ?? true; const compat = model.compat as { supportsDeveloperRole?: boolean | undefined } | undefined; 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") { if (systemIndex++ >= transcriptTools.foldedSystemMessages) appendSystemToolAdditions(msg, `system:${msgIndex}`); if (!isLeadingSystemMessage || includeSystemPrompt) { const text = isLeadingSystemMessage ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg); 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) => item.type === "text" ? { type: "input_text" as const, text: sanitizeSurrogates(item.text) } : { type: "input_image" as const, detail: imageDetailForResponses(item), image_url: `data:${item.mimeType};base64,${item.data}` }, ); if (content.length > 0) messages.push({ role: "user", content }); } } else if (msg.role === "assistant") { const output: ResponseInput = []; const isSameProviderAndApi = msg.provider === model.provider && msg.api === model.api; const isSameModel = isSameProviderAndApi && msg.model === model.id; const isDifferentModel = isSameProviderAndApi && msg.model !== model.id; let textBlockIndex = 0; for (const block of msg.content as InternalAssistantContent[]) { if (isImageGenerationCallBlock(block)) { const imageGenerationCall = sanitizeImageGenerationCallItem(block.item); if (imageGenerationCall) output.push(imageGenerationCall as ResponseInput[number]); } else if (isWebSearchCallBlock(block)) { const webSearchCall = sanitizeWebSearchCallItem(block.item); if (webSearchCall) output.push(webSearchCall as ResponseInput[number]); } else if (block.type === "thinking") { const thinkingItem = block.thinkingSignature ? parseResponsesThinkingSignature(block.thinkingSignature) : undefined; if (thinkingItem) output.push(thinkingItem); } else if (block.type === "text") { const parsedSignature = parseTextSignature(block.textSignature); const fallbackMessageId = textBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`; textBlockIndex++; let msgId = parsedSignature?.id ?? fallbackMessageId; if (msgId.length > 64) msgId = `msg_${shortHash(msgId)}`; output.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: sanitizeSurrogates(block.text), annotations: [] }], status: "completed", id: msgId, ...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}), }); } else if (block.type === "toolCall") { const wireCall = unrouteContextNamespaceToolCall(block); const [callId, itemIdRaw] = block.id.split("|"); const customInputProperty = recordedCustomInputs.get(block.id) ?? options?.grammarToolInputProperties?.get(block.name); if (customInputProperty !== undefined && callId !== undefined) customCallIds.add(callId); let itemId: string | undefined = itemIdRaw; if (customInputProperty !== undefined && itemId?.startsWith("fc_")) { itemId = `ctc_${itemId.slice(3)}`; } const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_"; if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) itemId = undefined; const namespace = recordedResponsesNamespace(block) ?? (isSameModel || anchoredToolNames.has(block.name) ? block.namespace : undefined); output.push(customInputProperty === undefined ? { type: "function_call", ...(itemId ? { id: itemId } : {}), call_id: callId, name: wireCall.name, arguments: JSON.stringify(wireCall.arguments), ...(namespace !== undefined ? { namespace } : {}), } as ResponseInput[number] : { type: "custom_tool_call", ...(itemId ? { id: itemId } : {}), call_id: callId, name: wireCall.name, input: sanitizeSurrogates(getGrammarToolInput(block.name, wireCall.arguments, customInputProperty)), ...(namespace !== undefined ? { namespace } : {}), } as ResponseInput[number]); } } if (output.length > 0) messages.push(...output); } else if (msg.role === "toolResult") { const textResult = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n"); const hasImages = msg.content.some((c) => c.type === "image"); const hasText = textResult.length > 0; const [callId] = msg.toolCallId.split("|"); const encryptedToolOutput = encryptedToolOutputFromDetails(msg.details); const attachmentHint = (msg.details as { codexHistoryNotes?: { attachment_hint?: unknown } } | undefined)?.codexHistoryNotes?.attachment_hint; const opaqueOutputs = opaqueToolOutputsFromDetails(msg.details); if (opaqueOutputs.length && callId !== undefined && customCallIds.has(callId)) throw new Error("Protected results require a native tool output"); const output = opaqueOutputs.length ? [ ...(hasText ? [{ type: "input_text" as const, text: sanitizeSurrogates(textResult) }] : []), ...opaqueOutputs.flatMap(item => [ { type: "input_text" as const, text: `Result ${item.resultId} (${item.name})` }, { type: "encrypted_content" as const, encrypted_content: item.encryptedOutput }, ]), ...(model.input.includes("image") ? msg.content .filter((block): block is ImageContentWithDetail => block.type === "image") .map(block => ({ type: "input_image" as const, detail: imageDetailForResponses(block), image_url: `data:${block.mimeType};base64,${block.data}` })) : []), ] : encryptedToolOutput ? [ ...(typeof attachmentHint === "string" ? [{ type: "input_text" as const, text: sanitizeSurrogates(attachmentHint) }] : []), { type: "encrypted_content" as const, encrypted_content: encryptedToolOutput }, ...(hasImages && model.input.includes("image") ? msg.content .filter((block): block is ImageContentWithDetail => block.type === "image") .map((block) => ({ type: "input_image" as const, detail: imageDetailForResponses(block), image_url: `data:${block.mimeType};base64,${block.data}`, })) : []), ] : hasImages && model.input.includes("image") ? [ ...(hasText ? [{ type: "input_text" as const, text: sanitizeSurrogates(textResult) }] : []), ...msg.content .filter((block): block is ImageContentWithDetail => block.type === "image") .map((block) => ({ type: "input_image" as const, detail: imageDetailForResponses(block), image_url: `data:${block.mimeType};base64,${block.data}`, })), ] : sanitizeSurrogates(hasText ? textResult : "(see attached image)"); messages.push({ type: callId !== undefined && customCallIds.has(callId) ? "custom_tool_call_output" : "function_call_output", call_id: callId!, output: output as any, } as ResponseInput[number]); } msgIndex++; } return normalizeResponsesToolHistory(messages) as ResponseInput; } export function convertResponsesTools(tools: readonly Tool[], options?: ConvertResponsesToolsOptions): OpenAITool[] { const defaultStrict = options?.strict === undefined ? false : options.strict; const supportsStrictMode = options?.supportsStrictMode ?? true; const supportsOpenAIGrammarTools = options?.supportsOpenAIGrammarTools ?? false; return tools.map((tool): OpenAITool => { 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 } : {}), } as OpenAITool; const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode); const strict = constrainedStrict ?? defaultStrict; const functionTool = { type: "function", name: tool.name, description: tool.description, parameters: getJsonSchemaToolParameters(tool, strict === true) as unknown as Record, ...(options?.toolSearchResult ? { defer_loading: true } : {}), } as Extract; if (supportsStrictMode) functionTool.strict = strict; return functionTool; }); } export { processResponsesStream } from "./stream.ts";