import { $env } from "@oh-my-pi/pi-utils"; import type { ResponseInput, ResponseInputItem } from "./providers/openai-responses-wire"; import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types"; type OpenAIResponsesReplayItem = ResponseInput[number]; export { isRecord } from "@oh-my-pi/pi-utils"; export function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[] { if (systemPrompt === undefined || systemPrompt === null) return []; const prompts = Array.isArray(systemPrompt) ? systemPrompt : typeof systemPrompt === "string" ? [systemPrompt] : []; return prompts.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.trim().length > 0); } export function normalizeToolCallId(id: string): string { const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_"); return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized; } type ResponsesToolItemIdPrefix = "fc" | "ctc"; export function normalizeResponsesToolCallId( id: string, itemPrefix: ResponsesToolItemIdPrefix = "fc", ): { callId: string; itemId: string } { const [callId, itemId] = id.split("|"); if (callId && itemId) { const normalizedCallId = truncateResponseItemId(callId, getIdPrefix(callId, "call")); const normalizedItemId = normalizeResponsesItemId(itemId, itemPrefix); return { callId: normalizedCallId, itemId: normalizedItemId }; } const hash = Bun.hash(id).toString(36); const normalizedCallId = id.startsWith("call_") ? truncateResponseItemId(id, "call") : `call_${hash}`; return { callId: normalizedCallId, itemId: `${itemPrefix}_${hash}` }; } function getIdPrefix(id: string, fallback: string): string { const prefix = id.match(/^([a-zA-Z][a-zA-Z0-9]*)_/)?.[1]; return prefix || fallback; } function getExplicitIdPrefix(id: string): string | undefined { return id.match(/^([a-zA-Z][a-zA-Z0-9]*)_/)?.[1]; } function normalizeResponsesItemId(itemId: string, fallbackPrefix: ResponsesToolItemIdPrefix): string { const prefix = getExplicitIdPrefix(itemId); const isAllowedPrefix = prefix ? fallbackPrefix === "ctc" ? prefix === "ctc" : prefix === "fc" || prefix === "fcr" : false; if (!prefix || !isAllowedPrefix) { return `${fallbackPrefix}_${Bun.hash(itemId).toString(36)}`; } return truncateResponseItemId(itemId, prefix); } /** * Truncate an OpenAI Responses API item ID to 64 characters. * IDs exceeding the limit are replaced with a hash-based ID using the given prefix. */ export function truncateResponseItemId(id: string, prefix: string): string { if (id.length <= 64) return id; return `${prefix}_${Bun.hash(id).toString(36)}`; } export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array>): ResponseInput { const normalizedCallIds = new Map(); return items.flatMap(item => { const sanitized = sanitizeOpenAIResponsesHistoryItemForReplay(item, normalizedCallIds); return sanitized ? [sanitized] : []; }); } function sanitizeOpenAIResponsesHistoryItemForReplay( item: Record, normalizedCallIds: Map, ): OpenAIResponsesReplayItem | undefined { if (item.type === "item_reference") return undefined; if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item); if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item); // providerPayload stores raw output items; replay strips item ids and keeps only normalized call_id. const { id: _id, ...sanitizedItem } = item; if (typeof item.call_id === "string") { sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds); } return sanitizedItem as unknown as OpenAIResponsesReplayItem; } function sanitizeOpenAIResponsesReasoningItemForReplay(item: Record): OpenAIResponsesReplayItem { const sanitizedItem: Record = { type: "reasoning" }; if (Array.isArray(item.summary)) sanitizedItem.summary = item.summary; if (Array.isArray(item.content)) sanitizedItem.content = item.content; if (typeof item.encrypted_content === "string" || item.encrypted_content === null) { sanitizedItem.encrypted_content = item.encrypted_content; } if (item.status === "in_progress" || item.status === "completed" || item.status === "incomplete") { sanitizedItem.status = item.status; } return sanitizedItem as unknown as OpenAIResponsesReplayItem; } function sanitizeOpenAIResponsesImageGenerationCallForReplay( item: Record, ): ResponseInputItem.ImageGenerationCall | undefined { if (typeof item.id !== "string" || item.status !== "completed" || typeof item.result !== "string") { return undefined; } return { id: truncateResponseItemId(item.id, "ig"), type: "image_generation_call", status: "completed", result: item.result, }; } function normalizeReplayedResponsesHistoryCallId(value: string, normalizedValues: Map): string { const normalized = normalizedValues.get(value); if (normalized) return normalized; const next = truncateResponseItemId(value, getIdPrefix(value, "call")); normalizedValues.set(value, next); return next; } export function createOpenAIResponsesHistoryPayload( provider: string, items: Array>, incremental = true, ): OpenAIResponsesHistoryPayload { return { type: "openaiResponsesHistory", provider, ...(incremental ? { dt: true } : {}), items, }; } export function getOpenAIResponsesHistoryPayload( providerPayload: ProviderPayload | undefined, currentProvider: string, fallbackProvider?: string, ): OpenAIResponsesHistoryPayload | undefined { if (providerPayload?.type !== "openaiResponsesHistory" || !Array.isArray(providerPayload.items)) { return undefined; } const payloadProvider = providerPayload.provider ?? fallbackProvider; if (!payloadProvider || payloadProvider !== currentProvider) { return undefined; } return { ...providerPayload, provider: payloadProvider }; } export function getOpenAIResponsesHistoryItems( providerPayload: ProviderPayload | undefined, currentProvider: string, fallbackProvider?: string, ): Array> | undefined { return getOpenAIResponsesHistoryPayload(providerPayload, currentProvider, fallbackProvider)?.items; } /** * Resolve cache retention preference. * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. */ export function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention { if (cacheRetention) return cacheRetention; if ($env.PI_CACHE_RETENTION === "long") return "long"; return "short"; }