import type { ResponseInputItem, ResponseStreamEvent, ResponseInputContent } from "openai/resources/responses/responses"; import type { UnifiedMessage, UnifiedStreamEvent, FinishReason, ProviderId } from "@/lib/ai/core/types"; import { inlineTextFile, isTextLike } from "@/lib/ai/core/content"; import { safeJson } from "@/lib/ai/core/stream-utils"; /** * OpenAI Responses API helpers — used by OpenAI and by xAI's `/v1/responses`. * Conversation state is always sent in full (`store: false`), so BYOK users' prompts are * not retained provider-side beyond the request. */ export interface ResponsesBuildOptions { /** Include native PDF input (`input_file` with `file_data`). xAI needs an uploaded file id → inline instead. */ nativeFiles?: boolean; /** Replay reasoning items (OpenAI `encrypted_content`) for reasoning continuity across turns. */ replayReasoning?: boolean; } export function toResponsesInput(messages: UnifiedMessage[], opts: ResponsesBuildOptions = {}): ResponseInputItem[] { const out: ResponseInputItem[] = []; for (const m of messages) { if (m.role === "system") { out.push({ role: "developer", content: m.content.map((p) => (p.type === "text" ? p.text : "")).join("") }); continue; } if (m.role === "tool") { for (const p of m.content) { if (p.type === "tool-result") out.push({ type: "function_call_output", call_id: p.toolCallId, output: typeof p.result === "string" ? p.result : JSON.stringify(p.result ?? null) }); } continue; } if (m.role === "assistant") { for (const p of m.content) { if (p.type === "reasoning" && opts.replayReasoning && p.providerData && typeof p.providerData === "object" && (p.providerData as { encrypted_content?: string }).encrypted_content) { const pd = p.providerData as { id?: string; encrypted_content: string }; out.push({ type: "reasoning", id: pd.id ?? `rs_${Math.random().toString(36).slice(2)}`, summary: [], encrypted_content: pd.encrypted_content } as ResponseInputItem); } } const text = m.content.map((p) => (p.type === "text" ? p.text : "")).join(""); if (text) out.push({ role: "assistant", content: [{ type: "output_text", text, annotations: [] }] } as unknown as ResponseInputItem); for (const p of m.content) { if (p.type === "tool-call") out.push({ type: "function_call", call_id: p.id, name: p.name, arguments: p.argumentsText ?? JSON.stringify(p.arguments) }); } for (const p of m.content) { if (p.type === "tool-result") out.push({ type: "function_call_output", call_id: p.toolCallId, output: typeof p.result === "string" ? p.result : JSON.stringify(p.result ?? null) }); } continue; } const parts: ResponseInputContent[] = []; for (const p of m.content) { if (p.type === "text") parts.push({ type: "input_text", text: p.text }); else if (p.type === "image") parts.push({ type: "input_image", image_url: `data:${p.mimeType};base64,${p.data}`, detail: "auto" }); else if (p.type === "file") { if (p.mimeType === "application/pdf" && opts.nativeFiles) parts.push({ type: "input_file", filename: p.name, file_data: `data:application/pdf;base64,${p.data}` }); else if (isTextLike(p.mimeType, p.name)) parts.push({ type: "input_text", text: inlineTextFile(p.name, p.data) }); else parts.push({ type: "input_text", text: `[Attached file "${p.name}" (${p.mimeType}) — this model cannot read binary files of this type.]` }); } } if (parts.length) out.push({ role: "user", content: parts }); } return out; } function mapIncomplete(reason: string | null | undefined): FinishReason { if (reason === "max_output_tokens") return "length"; if (reason === "content_filter") return "content-filter"; return "other"; } interface ToolAcc { callId: string; name: string; args: string; } /** Normalize a Responses API event stream into UnifiedStreamEvents. */ export async function* normalizeResponsesStream(stream: AsyncIterable, provider: ProviderId): AsyncIterable { const tools = new Map(); // by item_id let finish: FinishReason = "stop"; let sawToolCall = false; let started = false; let emittedFinish = false; for await (const ev of stream) { switch (ev.type) { case "response.created": if (!started) { started = true; yield { type: "start", id: ev.response.id, model: ev.response.model }; } break; case "response.output_item.added": { const item = ev.item as { type: string; id?: string; call_id?: string; name?: string }; if (item.type === "function_call" && item.id) { tools.set(item.id, { callId: item.call_id ?? item.id, name: item.name ?? "", args: "" }); sawToolCall = true; yield { type: "tool-start", id: item.call_id ?? item.id, name: item.name ?? "" }; } else if (item.type === "web_search_call") yield { type: "server-tool", name: "web_search", status: "started" }; else if (item.type === "code_interpreter_call") yield { type: "server-tool", name: "code_interpreter", status: "started" }; else if (item.type === "file_search_call") yield { type: "server-tool", name: "file_search", status: "started" }; break; } case "response.output_text.delta": yield { type: "text-delta", text: ev.delta }; break; case "response.reasoning_summary_text.delta": yield { type: "reasoning-delta", text: ev.delta }; break; case "response.reasoning_summary_part.done": yield { type: "reasoning-delta", text: "\n\n" }; break; case "response.function_call_arguments.delta": { const acc = tools.get(ev.item_id); if (acc) { acc.args += ev.delta; yield { type: "tool-delta", id: acc.callId, argumentsDelta: ev.delta }; } break; } case "response.function_call_arguments.done": { const acc = tools.get(ev.item_id); if (acc) { acc.args = ev.arguments || acc.args; yield { type: "tool-end", id: acc.callId, name: acc.name, arguments: safeJson(acc.args), argumentsText: acc.args }; tools.delete(ev.item_id); } break; } case "response.output_item.done": { const item = ev.item as { type: string; id?: string; call_id?: string; name?: string; arguments?: string; encrypted_content?: string | null; summary?: unknown; action?: { sources?: { url: string; title?: string }[] } }; if (item.type === "function_call" && item.id && tools.has(item.id)) { const acc = tools.get(item.id)!; const args = item.arguments ?? acc.args; yield { type: "tool-end", id: acc.callId, name: acc.name, arguments: safeJson(args), argumentsText: args }; tools.delete(item.id); } else if (item.type === "reasoning") { if (item.encrypted_content) yield { type: "reasoning-signature", signature: "encrypted", providerData: { id: item.id, encrypted_content: item.encrypted_content } }; } else if (item.type === "web_search_call") { yield { type: "server-tool", name: "web_search", status: "completed", data: item.action?.sources?.slice(0, 10) }; } else if (item.type === "code_interpreter_call") { yield { type: "server-tool", name: "code_interpreter", status: "completed" }; } break; } case "response.output_text.annotation.added": { const a = ev.annotation as { type: string; url?: string; title?: string; start_index?: number; end_index?: number }; if (a.type === "url_citation") yield { type: "citation", citation: { url: a.url, title: a.title, startIndex: a.start_index, endIndex: a.end_index, source: "web_search" } }; break; } case "response.completed": case "response.incomplete": case "response.failed": { const r = ev.response; const u = r.usage; if (u) { yield { type: "usage", usage: { inputTokens: u.input_tokens ?? 0, outputTokens: u.output_tokens ?? 0, cachedInputTokens: u.input_tokens_details?.cached_tokens ?? undefined, reasoningTokens: u.output_tokens_details?.reasoning_tokens ?? undefined, totalTokens: u.total_tokens ?? undefined, }, }; } if (ev.type === "response.failed") { const err = (r as { error?: { code?: string; message?: string } | null }).error; yield { type: "error", error: { code: "UNKNOWN_PROVIDER_ERROR", message: err?.message ?? "Response failed", provider, retryable: false, providerCode: err?.code } }; emittedFinish = true; return; } if (ev.type === "response.incomplete") finish = mapIncomplete(r.incomplete_details?.reason); else finish = sawToolCall ? "tool-calls" : "stop"; if (r.id) yield { type: "provider-data", data: { responseId: r.id } }; break; } case "error": { const e = ev as { code?: string | null; message?: string }; yield { type: "error", error: { code: "UNKNOWN_PROVIDER_ERROR", message: e.message ?? "Stream error", provider, retryable: false, providerCode: e.code ?? undefined } }; emittedFinish = true; return; } default: break; } } if (!emittedFinish) yield { type: "finish", reason: finish }; }