TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { ResponseInputItem, ResponseStreamEvent, ResponseInputContent } from "openai/resources/responses/responses";2import type { UnifiedMessage, UnifiedStreamEvent, FinishReason, ProviderId } from "@/lib/ai/core/types";3import { inlineTextFile, isTextLike } from "@/lib/ai/core/content";4import { safeJson } from "@/lib/ai/core/stream-utils";56/**7 * OpenAI Responses API helpers — used by OpenAI and by xAI's `/v1/responses`.8 * Conversation state is always sent in full (`store: false`), so BYOK users' prompts are9 * not retained provider-side beyond the request.10 */1112export interface ResponsesBuildOptions {13 /** Include native PDF input (`input_file` with `file_data`). xAI needs an uploaded file id → inline instead. */14 nativeFiles?: boolean;15 /** Replay reasoning items (OpenAI `encrypted_content`) for reasoning continuity across turns. */16 replayReasoning?: boolean;17}1819export function toResponsesInput(messages: UnifiedMessage[], opts: ResponsesBuildOptions = {}): ResponseInputItem[] {20 const out: ResponseInputItem[] = [];21 for (const m of messages) {22 if (m.role === "system") {23 out.push({ role: "developer", content: m.content.map((p) => (p.type === "text" ? p.text : "")).join("") });24 continue;25 }26 if (m.role === "tool") {27 for (const p of m.content) {28 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) });29 }30 continue;31 }32 if (m.role === "assistant") {33 for (const p of m.content) {34 if (p.type === "reasoning" && opts.replayReasoning && p.providerData && typeof p.providerData === "object" && (p.providerData as { encrypted_content?: string }).encrypted_content) {35 const pd = p.providerData as { id?: string; encrypted_content: string };36 out.push({ type: "reasoning", id: pd.id ?? `rs_${Math.random().toString(36).slice(2)}`, summary: [], encrypted_content: pd.encrypted_content } as ResponseInputItem);37 }38 }39 const text = m.content.map((p) => (p.type === "text" ? p.text : "")).join("");40 if (text) out.push({ role: "assistant", content: [{ type: "output_text", text, annotations: [] }] } as unknown as ResponseInputItem);41 for (const p of m.content) {42 if (p.type === "tool-call") out.push({ type: "function_call", call_id: p.id, name: p.name, arguments: p.argumentsText ?? JSON.stringify(p.arguments) });43 }44 for (const p of m.content) {45 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) });46 }47 continue;48 }49 const parts: ResponseInputContent[] = [];50 for (const p of m.content) {51 if (p.type === "text") parts.push({ type: "input_text", text: p.text });52 else if (p.type === "image") parts.push({ type: "input_image", image_url: `data:${p.mimeType};base64,${p.data}`, detail: "auto" });53 else if (p.type === "file") {54 if (p.mimeType === "application/pdf" && opts.nativeFiles) parts.push({ type: "input_file", filename: p.name, file_data: `data:application/pdf;base64,${p.data}` });55 else if (isTextLike(p.mimeType, p.name)) parts.push({ type: "input_text", text: inlineTextFile(p.name, p.data) });56 else parts.push({ type: "input_text", text: `[Attached file "${p.name}" (${p.mimeType}) — this model cannot read binary files of this type.]` });57 }58 }59 if (parts.length) out.push({ role: "user", content: parts });60 }61 return out;62}6364function mapIncomplete(reason: string | null | undefined): FinishReason {65 if (reason === "max_output_tokens") return "length";66 if (reason === "content_filter") return "content-filter";67 return "other";68}6970interface ToolAcc {71 callId: string;72 name: string;73 args: string;74}7576/** Normalize a Responses API event stream into UnifiedStreamEvents. */77export async function* normalizeResponsesStream(stream: AsyncIterable<ResponseStreamEvent>, provider: ProviderId): AsyncIterable<UnifiedStreamEvent> {78 const tools = new Map<string, ToolAcc>(); // by item_id79 let finish: FinishReason = "stop";80 let sawToolCall = false;81 let started = false;82 let emittedFinish = false;83 for await (const ev of stream) {84 switch (ev.type) {85 case "response.created":86 if (!started) {87 started = true;88 yield { type: "start", id: ev.response.id, model: ev.response.model };89 }90 break;91 case "response.output_item.added": {92 const item = ev.item as { type: string; id?: string; call_id?: string; name?: string };93 if (item.type === "function_call" && item.id) {94 tools.set(item.id, { callId: item.call_id ?? item.id, name: item.name ?? "", args: "" });95 sawToolCall = true;96 yield { type: "tool-start", id: item.call_id ?? item.id, name: item.name ?? "" };97 } else if (item.type === "web_search_call") yield { type: "server-tool", name: "web_search", status: "started" };98 else if (item.type === "code_interpreter_call") yield { type: "server-tool", name: "code_interpreter", status: "started" };99 else if (item.type === "file_search_call") yield { type: "server-tool", name: "file_search", status: "started" };100 break;101 }102 case "response.output_text.delta":103 yield { type: "text-delta", text: ev.delta };104 break;105 case "response.reasoning_summary_text.delta":106 yield { type: "reasoning-delta", text: ev.delta };107 break;108 case "response.reasoning_summary_part.done":109 yield { type: "reasoning-delta", text: "\n\n" };110 break;111 case "response.function_call_arguments.delta": {112 const acc = tools.get(ev.item_id);113 if (acc) {114 acc.args += ev.delta;115 yield { type: "tool-delta", id: acc.callId, argumentsDelta: ev.delta };116 }117 break;118 }119 case "response.function_call_arguments.done": {120 const acc = tools.get(ev.item_id);121 if (acc) {122 acc.args = ev.arguments || acc.args;123 yield { type: "tool-end", id: acc.callId, name: acc.name, arguments: safeJson(acc.args), argumentsText: acc.args };124 tools.delete(ev.item_id);125 }126 break;127 }128 case "response.output_item.done": {129 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 }[] } };130 if (item.type === "function_call" && item.id && tools.has(item.id)) {131 const acc = tools.get(item.id)!;132 const args = item.arguments ?? acc.args;133 yield { type: "tool-end", id: acc.callId, name: acc.name, arguments: safeJson(args), argumentsText: args };134 tools.delete(item.id);135 } else if (item.type === "reasoning") {136 if (item.encrypted_content) yield { type: "reasoning-signature", signature: "encrypted", providerData: { id: item.id, encrypted_content: item.encrypted_content } };137 } else if (item.type === "web_search_call") {138 yield { type: "server-tool", name: "web_search", status: "completed", data: item.action?.sources?.slice(0, 10) };139 } else if (item.type === "code_interpreter_call") {140 yield { type: "server-tool", name: "code_interpreter", status: "completed" };141 }142 break;143 }144 case "response.output_text.annotation.added": {145 const a = ev.annotation as { type: string; url?: string; title?: string; start_index?: number; end_index?: number };146 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" } };147 break;148 }149 case "response.completed":150 case "response.incomplete":151 case "response.failed": {152 const r = ev.response;153 const u = r.usage;154 if (u) {155 yield {156 type: "usage",157 usage: {158 inputTokens: u.input_tokens ?? 0,159 outputTokens: u.output_tokens ?? 0,160 cachedInputTokens: u.input_tokens_details?.cached_tokens ?? undefined,161 reasoningTokens: u.output_tokens_details?.reasoning_tokens ?? undefined,162 totalTokens: u.total_tokens ?? undefined,163 },164 };165 }166 if (ev.type === "response.failed") {167 const err = (r as { error?: { code?: string; message?: string } | null }).error;168 yield { type: "error", error: { code: "UNKNOWN_PROVIDER_ERROR", message: err?.message ?? "Response failed", provider, retryable: false, providerCode: err?.code } };169 emittedFinish = true;170 return;171 }172 if (ev.type === "response.incomplete") finish = mapIncomplete(r.incomplete_details?.reason);173 else finish = sawToolCall ? "tool-calls" : "stop";174 if (r.id) yield { type: "provider-data", data: { responseId: r.id } };175 break;176 }177 case "error": {178 const e = ev as { code?: string | null; message?: string };179 yield { type: "error", error: { code: "UNKNOWN_PROVIDER_ERROR", message: e.message ?? "Stream error", provider, retryable: false, providerCode: e.code ?? undefined } };180 emittedFinish = true;181 return;182 }183 default:184 break;185 }186 }187 if (!emittedFinish) yield { type: "finish", reason: finish };188}189