export interface LlmContextNormalizationInput { requestPayload: unknown; responsePayload: unknown; createdAt: number; } export interface LlmContextSummary { provider: "openai" | "anthropic" | "gemini"; model?: string; inputTokens?: number; outputTokens?: number; cacheCreationInputTokens?: number; cacheReadInputTokens?: number; stopReason?: string; requestMessageCount?: number; requestToolCount?: number; responseMessageCount?: number; responseToolCallCount?: number; responsePreview?: string; toolCallNames?: string[]; estimatedCostUsd?: number | null; } export interface LlmContextSection { kind: | "system" | "message" | "reasoning" | "settings" | "tool_definitions" | "tool_use" | "tool_result" | "function_call" | "function_response"; label: string; role?: string; text?: string; toolName?: string; data?: unknown; language?: string; } /** * Structured error extracted from a rejected call's response payload. * Mirrors the on-disk `responsePayload.error` shape produced by * `buildProviderErrorResponsePayload`. Present only when the call failed * before returning a response — consumers branch on its presence to * render the call as failed. */ export interface LlmContextError { name?: string; message?: string; code?: string; provider?: string; statusCode?: number; retryAfterMs?: number; apiErrorCode?: string; apiErrorType?: string; apiErrorParam?: string; requestId?: string; } export interface LlmContextNormalizationResult { summary?: LlmContextSummary; requestSections?: LlmContextSection[]; responseSections?: LlmContextSection[]; error?: LlmContextError; } interface NormalizedPayloadCandidate { provider: LlmContextSummary["provider"]; summary: LlmContextSummary; requestSections?: LlmContextSection[]; responseSections?: LlmContextSection[]; } export function normalizeLlmContextPayloads( input: LlmContextNormalizationInput, ): LlmContextNormalizationResult { const base = normalizeSuccessPayloads(input); const error = normalizeProviderErrorPayload(input.responsePayload); if (!error) { return base; } // A rejected call has no response sections to render — only the request // side normalized. Attach the structured error so the inspector can show // a failure banner and treat the cost as $0.00 instead of silently // falling back to "section rendering unavailable". return { ...base, error }; } /** * Detect a provider/transport error stored in the response payload. * * Error rows are written as `{ error: { name, message, code?, provider?, * statusCode?, retryAfterMs? } }` by `buildProviderErrorResponsePayload`. * Successful provider responses never carry a top-level `error` object, so * the presence of one (with at least one identifying field) is a reliable * signal that the call failed. */ function normalizeProviderErrorPayload( responsePayload: unknown, ): LlmContextError | null { const error = asRecord(asRecord(responsePayload)?.error); if (!error) { return null; } const name = asString(error.name); const message = asString(error.message); const code = asString(error.code); // Require at least one identifying field so an unrelated `error` key on a // success payload isn't misread as a provider failure. if (name === undefined && message === undefined && code === undefined) { return null; } const normalized: LlmContextError = {}; if (name !== undefined) { normalized.name = name; } if (message !== undefined) { normalized.message = message; } if (code !== undefined) { normalized.code = code; } const provider = asString(error.provider); if (provider !== undefined) { normalized.provider = provider; } const statusCode = asNumber(error.statusCode); if (statusCode !== undefined) { normalized.statusCode = statusCode; } const retryAfterMs = asNumber(error.retryAfterMs); if (retryAfterMs !== undefined) { normalized.retryAfterMs = retryAfterMs; } const apiErrorCode = asString(error.apiErrorCode); if (apiErrorCode !== undefined) { normalized.apiErrorCode = apiErrorCode; } const apiErrorType = asString(error.apiErrorType); if (apiErrorType !== undefined) { normalized.apiErrorType = apiErrorType; } const apiErrorParam = asString(error.apiErrorParam); if (apiErrorParam !== undefined) { normalized.apiErrorParam = apiErrorParam; } const requestId = asString(error.requestId); if (requestId !== undefined) { normalized.requestId = requestId; } return normalized; } function normalizeSuccessPayloads( input: LlmContextNormalizationInput, ): LlmContextNormalizationResult { const requestCandidates = [ normalizeOpenAiRequestPayload(input.requestPayload), normalizeAnthropicRequestPayload(input.requestPayload), normalizeGeminiRequestPayload(input.requestPayload), ].filter((candidate): candidate is NormalizedPayloadCandidate => Boolean(candidate), ); const responseCandidates = [ normalizeOpenAiResponsePayload(input.responsePayload), normalizeAnthropicResponsePayload(input.responsePayload), normalizeGeminiResponsePayload(input.responsePayload), ].filter((candidate): candidate is NormalizedPayloadCandidate => Boolean(candidate), ); if (requestCandidates.length > 1 || responseCandidates.length > 1) { return {}; } const requestCandidate = requestCandidates[0]; const responseCandidate = responseCandidates[0]; if (requestCandidate && responseCandidate) { if (requestCandidate.provider !== responseCandidate.provider) { return {}; } return mergeNormalizedCandidates(requestCandidate, responseCandidate); } if (requestCandidate) { const { summary, requestSections, responseSections } = requestCandidate; return { summary, requestSections, responseSections }; } if (responseCandidate) { const requestCandidate = normalizeCompatibleRequestPayload( input.requestPayload, responseCandidate.provider, ); if ( requestCandidate && requestCandidate.provider !== responseCandidate.provider ) { return {}; } return mergeNormalizedCandidates(requestCandidate, responseCandidate); } return {}; } function normalizeOpenAiRequestPayload( requestPayload: unknown, allowPlainText = false, ): NormalizedPayloadCandidate | null { // Try Responses API shape first, then fall back to chat-completions. return ( normalizeOpenAiResponsesRequestPayload(requestPayload, allowPlainText) ?? normalizeOpenAiChatCompletionsRequestPayload(requestPayload, allowPlainText) ); } /** * Detect and normalize OpenAI Responses API request payloads. * * Responses requests use `input` (array) instead of `messages`, may have a * top-level `instructions` string, and tools have `type: "function"` at the * top level with the function fields inlined (no nested `function` wrapper). */ function normalizeOpenAiResponsesRequestPayload( requestPayload: unknown, allowPlainText = false, ): NormalizedPayloadCandidate | null { const request = asRecord(requestPayload); if (!request) { return null; } const input = asRecordArray(request.input); if (!input) { return null; } // Require at least one Responses-specific signal to avoid matching generic // arrays. `instructions` is the strongest signal; otherwise look for // Responses-shaped input items or tool objects. const hasResponsesSignal = typeof request.instructions === "string" || hasOpenAiModelPrefix(asString(request.model)) || input.some( (item) => asString(item.type) === "function_call" || asString(item.type) === "function_call_output", ) || extractOpenAiResponsesRequestToolNames(request.tools).length > 0; if (!allowPlainText && !hasResponsesSignal) { return null; } const requestSections: LlmContextSection[] = []; // System-level instructions const instructions = asString(request.instructions); if (instructions && hasMeaningfulText(instructions)) { requestSections.push({ kind: "system", label: "System prompt", role: "system", text: instructions, }); } // Input items let messageIndex = 0; for (const item of input) { const itemType = asString(item.type); if (itemType === "message") { messageIndex++; const role = asString(item.role) ?? "unknown"; const messageText = extractOpenAiContentText(item.content); if (messageText !== undefined) { requestSections.push({ kind: "message", label: buildMessageLabel(role, messageIndex), role, text: messageText, }); } continue; } if (itemType === "function_call") { const name = asString(item.name); const args = parseJsonValue(asString(item.arguments)); requestSections.push({ kind: "function_call", label: `Request tool call${name ? ` (${name})` : ""}`, role: "assistant", toolName: name, data: args, text: previewStructuredValue(args), }); continue; } if (itemType === "function_call_output") { const output = asString(item.output); requestSections.push({ kind: "tool_result", label: `Tool result${asString(item.call_id) ? ` (${asString(item.call_id)})` : ""}`, role: "tool", toolName: asString(item.call_id), text: output, }); } } // Tool definitions (Responses shape: top-level type/name/parameters) const requestToolNames = extractOpenAiResponsesRequestToolNames( request.tools, ); if (requestToolNames.length > 0) { requestSections.push({ kind: "tool_definitions", label: "Available tools", data: { tools: asRecordArray(request.tools) ?? request.tools, }, language: "json", }); } const requestSettings = omitRecordKeys(request, [ "instructions", "input", "tools", ]); if (hasMeaningfulRequestSettings(requestSettings)) { requestSections.push( structuredJsonSection("settings", "Request settings", requestSettings), ); } // Count all input items for the message count (includes messages, function // calls, and function call outputs — mirrors how chat-completions counts // all messages regardless of role). return { provider: "openai", summary: { provider: "openai", model: asString(request.model), inputTokens: undefined, outputTokens: undefined, cacheCreationInputTokens: undefined, cacheReadInputTokens: undefined, stopReason: undefined, requestMessageCount: input.length, requestToolCount: requestToolNames.length, responseMessageCount: undefined, responseToolCallCount: undefined, responsePreview: undefined, toolCallNames: undefined, }, requestSections: requestSections.length > 0 ? requestSections : undefined, }; } /** * Normalize a legacy OpenAI Chat Completions request payload. * Requires `messages` array in the request. */ function normalizeOpenAiChatCompletionsRequestPayload( requestPayload: unknown, allowPlainText = false, ): NormalizedPayloadCandidate | null { const request = asRecord(requestPayload); if (!request) { return null; } const messages = asRecordArray(request.messages); if (!messages) { return null; } const requestToolNames = extractOpenAiRequestToolNames(request.tools); const hasOpenAiSignal = hasOpenAiModelPrefix(asString(request.model)) || requestToolNames.length > 0 || asString(request.tool_choice) !== undefined || (request.parallel_tool_calls !== undefined && typeof request.parallel_tool_calls === "boolean") || messages.some((message) => Boolean(asRecordArray(message.tool_calls))); if (!allowPlainText && !hasOpenAiSignal) { return null; } const requestSections: LlmContextSection[] = []; for (const [index, message] of messages.entries()) { const role = asString(message.role) ?? "unknown"; const messageText = extractOpenAiContentText(message.content); if (messageText !== undefined) { requestSections.push({ kind: role === "system" ? "system" : role === "tool" ? "tool_result" : "message", label: buildMessageLabel(role, index + 1), role, text: messageText, }); } for (const toolCallSection of openAiToolCallSections( message.tool_calls, "Request tool call", )) { requestSections.push(toolCallSection); } } if (requestToolNames.length > 0) { requestSections.push({ kind: "tool_definitions", label: "Available tools", data: { tools: asRecordArray(request.tools) ?? request.tools, }, language: "json", }); } const requestSettings = omitRecordKeys(request, ["messages", "tools"]); if (hasMeaningfulRequestSettings(requestSettings)) { requestSections.push( structuredJsonSection("settings", "Request settings", requestSettings), ); } return { provider: "openai", summary: { provider: "openai", model: asString(request.model), inputTokens: undefined, outputTokens: undefined, cacheCreationInputTokens: undefined, cacheReadInputTokens: undefined, stopReason: undefined, requestMessageCount: messages.length, requestToolCount: requestToolNames.length, responseMessageCount: undefined, responseToolCallCount: undefined, responsePreview: undefined, toolCallNames: undefined, }, requestSections: requestSections.length > 0 ? requestSections : undefined, }; } function normalizeOpenAiResponsePayload( responsePayload: unknown, ): NormalizedPayloadCandidate | null { // Try Responses API shape first, then fall back to chat-completions. return ( normalizeOpenAiResponsesResponsePayload(responsePayload) ?? normalizeOpenAiChatCompletionsResponsePayload(responsePayload) ); } /** * Detect and normalize OpenAI Responses API response payloads. * * Responses responses have an `output` array of items instead of `choices`. * Tool calls are top-level items with `type: "function_call"`. * Usage uses `input_tokens`/`output_tokens` (not `prompt_tokens`/`completion_tokens`). * `status` replaces `choices[0].finish_reason`. */ function normalizeOpenAiResponsesResponsePayload( responsePayload: unknown, ): NormalizedPayloadCandidate | null { const response = asRecord(responsePayload); if (!response) { return null; } const output = asRecordArray(response.output); if (!output) { return null; } // Require at least one Responses-specific signal const hasResponsesSignal = typeof response.status === "string" || hasOpenAiModelPrefix(asString(response.model)) || output.some( (item) => asString(item.type) === "message" || asString(item.type) === "function_call" || asString(item.type) === "web_search_call", ); if (!hasResponsesSignal) { return null; } const responseSections: LlmContextSection[] = []; let responseText: string | undefined; const toolCallSections: LlmContextSection[] = []; let toolCallIndex = 0; for (const item of output) { const itemType = asString(item.type); if (itemType === "message") { const role = asString(item.role) ?? "assistant"; const content = asRecordArray(item.content); const text = content ? extractOpenAiContentText(content) : undefined; if (text !== undefined) { responseText = text; responseSections.push({ kind: "message", label: "Assistant response", role, text, }); } continue; } if (itemType === "function_call") { toolCallIndex++; const name = asString(item.name); const args = parseJsonValue(asString(item.arguments)); const section: LlmContextSection = { kind: "function_call", label: `Response tool call ${toolCallIndex}`, role: "assistant", toolName: name, data: args, text: previewStructuredValue(args), }; toolCallSections.push(section); responseSections.push(section); continue; } if (itemType === "web_search_call") { toolCallIndex++; const status = asString(item.status); const section: LlmContextSection = { kind: "tool_use", label: `Response tool call ${toolCallIndex}`, role: "assistant", toolName: "web_search", data: omitRecordKeys(item, ["type"]) ?? undefined, text: status ? `[Web search: ${status}]` : "[Web search]", }; toolCallSections.push(section); responseSections.push(section); continue; } } const usage = asRecord(response.usage); const inputTokensDetails = asRecord(usage?.input_tokens_details); const toolCallNames = toolCallSections .map((section) => section.toolName) .filter((name): name is string => typeof name === "string"); // Map Responses API status to a stop reason string. const status = asString(response.status); const stopReason = status === "completed" ? "stop" : status; return { provider: "openai", summary: { provider: "openai", model: asString(response.model), inputTokens: asNumber(usage?.input_tokens), outputTokens: asNumber(usage?.output_tokens), cacheCreationInputTokens: undefined, cacheReadInputTokens: asNumber(inputTokensDetails?.cached_tokens), stopReason, requestMessageCount: undefined, requestToolCount: undefined, responseMessageCount: responseText !== undefined || toolCallSections.length > 0 ? 1 : undefined, responseToolCallCount: toolCallSections.length > 0 ? toolCallSections.length : undefined, responsePreview: responseText ? truncateText(responseText) : undefined, toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined, }, responseSections: responseSections.length > 0 ? responseSections : undefined, }; } /** * Normalize a legacy OpenAI Chat Completions response payload. * Requires `choices` array in the response. */ function normalizeOpenAiChatCompletionsResponsePayload( responsePayload: unknown, ): NormalizedPayloadCandidate | null { const response = asRecord(responsePayload); if (!response) { return null; } const choices = asRecordArray(response.choices); if (!choices) { return null; } const firstChoice = choices[0]; const responseMessage = asRecord(firstChoice?.message); const responseText = extractOpenAiContentText(responseMessage?.content); const responseSections: LlmContextSection[] = []; if (responseText !== undefined) { responseSections.push({ kind: "message", label: "Assistant response", role: asString(responseMessage?.role) ?? "assistant", text: responseText, }); } const responseToolSections = openAiToolCallSections( responseMessage?.tool_calls, "Response tool call", ); responseSections.push(...responseToolSections); const usage = asRecord(response.usage); const promptTokensDetails = asRecord(usage?.prompt_tokens_details); const toolCallNames = responseToolSections .map((section) => section.toolName) .filter((name): name is string => typeof name === "string"); return { provider: "openai", summary: { provider: "openai", model: asString(response.model), inputTokens: asNumber(usage?.prompt_tokens), outputTokens: asNumber(usage?.completion_tokens), cacheCreationInputTokens: undefined, cacheReadInputTokens: asNumber(promptTokensDetails?.cached_tokens), stopReason: asString(firstChoice?.finish_reason), requestMessageCount: undefined, requestToolCount: undefined, responseMessageCount: responseText !== undefined || responseToolSections.length > 0 ? 1 : undefined, responseToolCallCount: responseToolSections.length > 0 ? responseToolSections.length : undefined, responsePreview: responseText ? truncateText(responseText) : undefined, toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined, }, responseSections: responseSections.length > 0 ? responseSections : undefined, }; } function normalizeAnthropicRequestPayload( requestPayload: unknown, allowPlainText = false, ): NormalizedPayloadCandidate | null { const request = asRecord(requestPayload); if (!request) { return null; } const messages = asRecordArray(request.messages); if (!messages) { return null; } const requestToolNames = extractAnthropicToolNames(request.tools); const hasAnthropicContentSignal = messages.some((message) => (asRecordArray(message.content) ?? []).some((block) => { const type = asString(block.type); return ( type === "document" || type === "tool_use" || type === "server_tool_use" || type === "tool_result" || type === "web_search_tool_result" || type === "thinking" || type === "redacted_thinking" ); }), ); const hasAnthropicSignal = hasAnthropicModelPrefix(asString(request.model)) || request.system !== undefined || requestToolNames.length > 0 || isAnthropicToolChoice(request.tool_choice) || hasAnthropicContentSignal; if (!allowPlainText && !hasAnthropicSignal) { return null; } const requestSections: LlmContextSection[] = []; const systemSections = anthropicSystemSections(request.system); requestSections.push(...systemSections); for (const [index, message] of messages.entries()) { requestSections.push( ...anthropicMessageSections( message, buildMessageLabel(asString(message.role) ?? "unknown", index + 1), ), ); } if (requestToolNames.length > 0) { requestSections.push({ kind: "tool_definitions", label: "Available tools", data: { tools: asRecordArray(request.tools) ?? request.tools, }, language: "json", }); } const requestSettings = omitRecordKeys(request, [ "system", "messages", "tools", ]); if (hasMeaningfulRequestSettings(requestSettings)) { requestSections.push( structuredJsonSection("settings", "Request settings", requestSettings), ); } return { provider: "anthropic", summary: { provider: "anthropic", model: asString(request.model), inputTokens: undefined, outputTokens: undefined, cacheCreationInputTokens: undefined, cacheReadInputTokens: undefined, stopReason: undefined, requestMessageCount: messages.length, requestToolCount: requestToolNames.length, responseMessageCount: undefined, responseToolCallCount: undefined, responsePreview: undefined, toolCallNames: undefined, }, requestSections: requestSections.length > 0 ? requestSections : undefined, }; } function normalizeAnthropicResponsePayload( responsePayload: unknown, ): NormalizedPayloadCandidate | null { const response = asRecord(responsePayload); if (!response) { return null; } const content = asRecordArray(response.content); if (!content) { return null; } const responseSections = anthropicContentSections( content, "Assistant response", ); const responseText = collectAnthropicPreviewText(content); const responseToolNames = content .map((block) => isAnthropicToolUseType(asString(block.type)) ? asString(block.name) : undefined, ) .filter((name): name is string => typeof name === "string"); const hasAnthropicResponseMessage = responseSections.some( (section) => section.kind === "message" || section.kind === "tool_use" || section.kind === "reasoning", ); const usage = asRecord(response.usage); return { provider: "anthropic", summary: { provider: "anthropic", model: asString(response.model), inputTokens: asNumber(usage?.input_tokens), outputTokens: asNumber(usage?.output_tokens), cacheCreationInputTokens: asNumber(usage?.cache_creation_input_tokens), cacheReadInputTokens: asNumber(usage?.cache_read_input_tokens), stopReason: asString(response.stop_reason), requestMessageCount: undefined, requestToolCount: undefined, responseMessageCount: hasAnthropicResponseMessage ? 1 : undefined, responseToolCallCount: responseToolNames.length > 0 ? responseToolNames.length : undefined, responsePreview: responseText ? truncateText(responseText) : undefined, toolCallNames: responseToolNames.length > 0 ? responseToolNames : undefined, }, responseSections: responseSections.length > 0 ? responseSections : undefined, }; } function normalizeGeminiRequestPayload( requestPayload: unknown, ): NormalizedPayloadCandidate | null { const request = asRecord(requestPayload); if (!request) { return null; } const contents = asRecordArray(request.contents); if (!contents) { return null; } const requestSections: LlmContextSection[] = []; const config = asRecord(request.config); const systemText = extractGeminiSystemInstructionText( config?.systemInstruction, ); if (systemText !== undefined) { requestSections.push({ kind: "system", label: "System instruction", role: "system", text: systemText, }); } for (const [index, content] of contents.entries()) { requestSections.push(...geminiContentSections(content, index + 1)); } const requestToolNames = extractGeminiToolNames(config?.tools); if (requestToolNames.length > 0) { requestSections.push({ kind: "tool_definitions", label: "Available tools", data: { tools: asRecordArray(config?.tools) ?? config?.tools, }, language: "json", }); } const requestSettings = buildGeminiRequestSettings(request, config); if (hasMeaningfulRequestSettings(requestSettings)) { requestSections.push( structuredJsonSection("settings", "Generation config", requestSettings), ); } return { provider: "gemini", summary: { provider: "gemini", model: asString(request.model), inputTokens: undefined, outputTokens: undefined, cacheCreationInputTokens: undefined, cacheReadInputTokens: undefined, stopReason: undefined, requestMessageCount: contents.length, requestToolCount: requestToolNames.length, responseMessageCount: undefined, responseToolCallCount: undefined, responsePreview: undefined, toolCallNames: undefined, }, requestSections: requestSections.length > 0 ? requestSections : undefined, }; } function normalizeGeminiResponsePayload( responsePayload: unknown, ): NormalizedPayloadCandidate | null { const response = asRecord(responsePayload); if (!response) { return null; } const responseText = asString(response.text); const responseFunctionSections = geminiFunctionCallSections( response.functionCalls, "Response function call", ); const usage = asRecord(response.usageMetadata); if (responseText === undefined && responseFunctionSections.length === 0) { return null; } const responseSections: LlmContextSection[] = []; if (responseText !== undefined) { responseSections.push({ kind: "message", label: "Assistant response", role: "model", text: responseText, }); } responseSections.push(...responseFunctionSections); const toolCallNames = responseFunctionSections .map((section) => section.toolName) .filter((name): name is string => typeof name === "string"); return { provider: "gemini", summary: { provider: "gemini", model: asString(response.model), inputTokens: asNumber(usage?.promptTokenCount), outputTokens: asNumber(usage?.candidatesTokenCount), cacheCreationInputTokens: undefined, cacheReadInputTokens: undefined, stopReason: asString(response.finishReason), requestMessageCount: undefined, requestToolCount: undefined, responseMessageCount: responseText !== undefined || responseFunctionSections.length > 0 ? 1 : undefined, responseToolCallCount: responseFunctionSections.length > 0 ? responseFunctionSections.length : undefined, responsePreview: responseText ? truncateText(responseText) : undefined, toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined, }, responseSections: responseSections.length > 0 ? responseSections : undefined, }; } function anthropicSystemSections(system: unknown): LlmContextSection[] { const text = extractAnthropicSystemText(system); if (!text) { return []; } return [ { kind: "system", label: "System prompt", role: "system", text, }, ]; } function anthropicMessageSections( message: Record, label: string, ): LlmContextSection[] { const role = asString(message.role) ?? "unknown"; const content = message.content; const sections: LlmContextSection[] = []; // Collect reasoning sections first so they appear before the message text. for (const block of asRecordArray(content) ?? []) { const type = asString(block.type); if (type === "thinking" || type === "redacted_thinking") { sections.push({ kind: "reasoning", label: `${label} reasoning`, role, text: collectAnthropicReasoningText(block), }); } } const text = collectAnthropicMessageText(content); if (text) { sections.push({ kind: "message", label, role, text, }); } for (const block of asRecordArray(content) ?? []) { const type = asString(block.type); if (isAnthropicToolUseType(type)) { sections.push({ kind: "tool_use", label: `${label} tool use`, role, toolName: asString(block.name), data: asRecord(block.input) ?? block.input, text: previewStructuredValue(block.input), }); continue; } if (isAnthropicToolResultType(type)) { sections.push({ kind: "tool_result", label: `${label} tool result`, role, toolName: asString(block.name) ?? asString(block.tool_use_id), data: type === "web_search_tool_result" ? sanitizeAnthropicWebSearchToolResultData(block) : undefined, text: collectAnthropicToolResultText(block), }); } } return sections; } function anthropicContentSections( content: Record[], label: string, ): LlmContextSection[] { return anthropicMessageSections( { role: "assistant", content, }, label, ); } function geminiContentSections( content: Record, index: number, ): LlmContextSection[] { const role = asString(content.role) ?? "unknown"; const parts = asRecordArray(content.parts) ?? []; const sections: LlmContextSection[] = []; const textParts: string[] = []; for (const part of parts) { const text = asString(part.text); if (text) { textParts.push(text); continue; } const inlineData = asRecord(part.inlineData); if (inlineData) { const mimeType = asString(inlineData.mimeType) ?? "application/octet-stream"; textParts.push(`[inline data: ${mimeType}]`); continue; } const functionCall = asRecord(part.functionCall); if (functionCall) { sections.push({ kind: "function_call", label: `${buildMessageLabel(role, index)} function call`, role, toolName: asString(functionCall.name), data: asRecord(functionCall.args) ?? functionCall.args, text: previewStructuredValue(functionCall.args), }); continue; } const functionResponse = asRecord(part.functionResponse); if (functionResponse) { sections.push({ kind: "function_response", label: `${buildMessageLabel(role, index)} function response`, role, toolName: asString(functionResponse.name), data: asRecord(functionResponse.response) ?? functionResponse.response, text: previewStructuredValue(functionResponse.response), }); } } const text = joinTextParts(textParts); if (text) { sections.unshift({ kind: "message", label: buildMessageLabel(role, index), role, text, }); } return sections; } function openAiToolCallSections( toolCalls: unknown, labelPrefix: string, ): LlmContextSection[] { return (asRecordArray(toolCalls) ?? []).map((toolCall, index) => { const fn = asRecord(toolCall.function); return { kind: "function_call", label: `${labelPrefix} ${index + 1}`, role: "assistant", toolName: asString(fn?.name), data: parseJsonValue(asString(fn?.arguments)), text: previewStructuredValue(parseJsonValue(asString(fn?.arguments))), }; }); } function geminiFunctionCallSections( functionCalls: unknown, labelPrefix: string, ): LlmContextSection[] { return (asRecordArray(functionCalls) ?? []).map((call, index) => ({ kind: "function_call", label: `${labelPrefix} ${index + 1}`, role: "model", toolName: asString(call.name), data: asRecord(call.args) ?? call.args, text: previewStructuredValue(call.args), })); } function extractOpenAiRequestToolNames(tools: unknown): string[] { return (asRecordArray(tools) ?? []) .map((tool) => asString(asRecord(tool.function)?.name)) .filter((name): name is string => typeof name === "string"); } /** * Extract tool names from Responses API tool definitions. * Responses tools have `type: "function"` at the top level with `name` * directly on the tool object (no nested `function` wrapper). */ function extractOpenAiResponsesRequestToolNames(tools: unknown): string[] { return (asRecordArray(tools) ?? []) .map((tool) => { // Responses shape: { type: "function", name: "...", ... } if (asString(tool.type) === "function" && asString(tool.name)) { return asString(tool.name); } // Native web search tools: // - OpenAI API: { type: "web_search_preview" } // - Codex subscription endpoint: { type: "web_search" } if ( asString(tool.type) === "web_search_preview" || asString(tool.type) === "web_search" ) { return "web_search"; } return undefined; }) .filter((name): name is string => typeof name === "string"); } function extractAnthropicToolNames(tools: unknown): string[] { return (asRecordArray(tools) ?? []) .map((tool) => asString(tool.name)) .filter((name): name is string => typeof name === "string"); } function isAnthropicToolChoice(toolChoice: unknown): boolean { const record = asRecord(toolChoice); if (!record) { return false; } const type = asString(record.type); return ( type === "auto" || type === "any" || type === "tool" || type === "none" ); } function extractGeminiToolNames(tools: unknown): string[] { const toolGroups = asRecordArray(tools) ?? []; const names: string[] = []; for (const toolGroup of toolGroups) { for (const declaration of asRecordArray(toolGroup.functionDeclarations) ?? []) { const name = asString(declaration.name); if (name) { names.push(name); } } } return names; } function extractOpenAiContentText(content: unknown): string | undefined { if (typeof content === "string") { return hasMeaningfulText(content) ? content : undefined; } const parts = asRecordArray(content); if (!parts) { return undefined; } const textParts: string[] = []; for (const part of parts) { const type = asString(part.type); if (type === "text" || type === "input_text" || type === "output_text") { const text = asString(part.text); if (text) { textParts.push(text); } continue; } if (type === "image_url" || type === "input_image") { textParts.push("[image]"); continue; } if (type === "file") { textParts.push("[file]"); } } return joinTextParts(textParts); } function extractAnthropicSystemText(system: unknown): string | undefined { if (typeof system === "string") { return hasMeaningfulText(system) ? system : undefined; } const parts = asRecordArray(system); if (!parts) { return undefined; } const textParts = parts .map((part) => asString(part.text)) .filter((text): text is string => typeof text === "string"); return joinTextParts(textParts); } function extractGeminiSystemInstructionText( systemInstruction: unknown, ): string | undefined { if (typeof systemInstruction === "string") { return hasMeaningfulText(systemInstruction) ? systemInstruction : undefined; } const record = asRecord(systemInstruction); if (!record) { return undefined; } const parts = asRecordArray(record.parts) ?? []; const textParts = parts .map((part) => asString(part.text)) .filter((text): text is string => typeof text === "string"); return joinTextParts(textParts); } function collectAnthropicText(content: unknown): string | undefined { if (typeof content === "string") { return hasMeaningfulText(content) ? content : undefined; } const blocks = asRecordArray(content); if (!blocks) { return undefined; } const textParts: string[] = []; for (const block of blocks) { const text = collectAnthropicBlockText(block); if (text) { textParts.push(text); } } return joinTextParts(textParts); } function collectAnthropicMessageText(content: unknown): string | undefined { if (typeof content === "string") { return hasMeaningfulText(content) ? content : undefined; } const blocks = asRecordArray(content); if (!blocks) { return undefined; } const textParts: string[] = []; for (const block of blocks) { const text = collectAnthropicBlockText(block); if (text) { textParts.push(text); } } return joinTextParts(textParts); } function collectAnthropicPreviewText(content: unknown): string | undefined { if (typeof content === "string") { return hasMeaningfulText(content) ? content : undefined; } const blocks = asRecordArray(content); if (!blocks) { return undefined; } const textParts: string[] = []; for (const block of blocks) { if (asString(block.type) !== "text") { continue; } const text = asString(block.text); if (text) { textParts.push(text); } } return joinTextParts(textParts); } function collectAnthropicBlockText( block: Record, ): string | undefined { const type = asString(block.type); if (type === "text") { const text = asString(block.text); return text ? text : undefined; } if (type === "image") { return "[image]"; } if (type === "document") { const title = asString(block.title); return title ? `[document: ${title}]` : "[document]"; } return undefined; } function collectAnthropicReasoningText( block: Record, ): string | undefined { const type = asString(block.type); if (type === "thinking") { const thinking = asString(block.thinking); return thinking ? thinking : undefined; } if (type === "redacted_thinking") { return "[redacted thinking]"; } return undefined; } function isAnthropicToolUseType(type: string | undefined): boolean { return type === "tool_use" || type === "server_tool_use"; } function isAnthropicToolResultType(type: string | undefined): boolean { return type === "tool_result" || type === "web_search_tool_result"; } function collectAnthropicToolResultText( block: Record, ): string | undefined { if (asString(block.type) === "web_search_tool_result") { return "[Web search results]"; } return collectAnthropicText(block.content); } function sanitizeAnthropicWebSearchToolResultData( block: Record, ): unknown { return sanitizeAnthropicStructuredValue(block); } function sanitizeAnthropicStructuredValue(value: unknown): unknown { if (Array.isArray(value)) { return value.map((entry) => sanitizeAnthropicStructuredValue(entry)); } const record = asRecord(value); if (!record) { return value; } return Object.fromEntries( Object.entries(record) .filter( ([key, entryValue]) => key !== "encrypted_content" && entryValue !== undefined, ) .map(([key, entryValue]) => [ key, sanitizeAnthropicStructuredValue(entryValue), ]), ); } function buildMessageLabel(role: string, index: number): string { const capitalizedRole = role.length > 0 ? role[0]!.toUpperCase() + role.slice(1) : "Message"; if (role === "system") { return "System prompt"; } return `${capitalizedRole} message ${index}`; } function buildGeminiRequestSettings( request: Record, config: Record | null, ): Record | undefined { const topLevelSettings = omitRecordKeys(request, ["contents", "config"]); const configSettings = omitRecordKeys(config, ["systemInstruction", "tools"]); if (!topLevelSettings && !configSettings) { return undefined; } return { ...(topLevelSettings ?? {}), ...(configSettings ? { config: configSettings } : {}), }; } function structuredJsonSection( kind: LlmContextSection["kind"], label: string, data: unknown, ): LlmContextSection { return { kind, label, data, language: "json", }; } function hasMeaningfulRequestSettings( settings: Record | undefined, ): settings is Record { if (!settings) { return false; } const keys = Object.keys(settings); return !(keys.length === 1 && keys[0] === "model"); } function omitRecordKeys( record: Record | null, omittedKeys: string[], ): Record | undefined { if (!record) { return undefined; } const filteredEntries = Object.entries(record).filter( ([key, value]) => !omittedKeys.includes(key) && value !== undefined, ); if (filteredEntries.length === 0) { return undefined; } return Object.fromEntries(filteredEntries); } function previewStructuredValue(value: unknown): string | undefined { if (value === undefined) { return undefined; } if (typeof value === "string") { return truncateText(value); } try { return truncateText(JSON.stringify(value)); } catch { return undefined; } } function parseJsonValue(value: string | undefined): unknown { if (!value) { return undefined; } try { return JSON.parse(value); } catch { return value; } } function joinTextParts(parts: string[]): string | undefined { if (parts.length === 0) { return undefined; } const text = parts.join("\n\n"); return hasMeaningfulText(text) ? text : undefined; } function truncateText(text: string, maxLength = 280): string { const normalized = normalizeText(text); if (normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, maxLength - 3).trimEnd()}...`; } function normalizeText(text: string): string { const normalized = text.replace(/\s+/g, " ").trim(); return normalized.length > 0 ? normalized : ""; } function hasMeaningfulText(text: string): boolean { return text.trim().length > 0; } function mergeSummaryFragments( requestSummary: LlmContextSummary | undefined, responseSummary: LlmContextSummary | undefined, ): LlmContextSummary | undefined { if (!requestSummary && !responseSummary) { return undefined; } const summary = { ...(requestSummary ?? responseSummary)!, } as LlmContextSummary; if (!requestSummary || !responseSummary) { return summary; } for (const [key, value] of Object.entries(responseSummary) as [ keyof LlmContextSummary, LlmContextSummary[keyof LlmContextSummary], ][]) { if (value !== undefined) { summary[key] = value as never; } } return summary; } function mergeNormalizedCandidates( requestCandidate: NormalizedPayloadCandidate | null | undefined, responseCandidate: NormalizedPayloadCandidate | null | undefined, ): LlmContextNormalizationResult { if (!requestCandidate && !responseCandidate) { return {}; } const requestSections = [ ...(requestCandidate?.requestSections ?? []), ...(responseCandidate?.requestSections ?? []), ]; const responseSections = [ ...(requestCandidate?.responseSections ?? []), ...(responseCandidate?.responseSections ?? []), ]; return { summary: mergeSummaryFragments( requestCandidate?.summary, responseCandidate?.summary, ), requestSections: requestSections.length > 0 ? requestSections : undefined, responseSections: responseSections.length > 0 ? responseSections : undefined, }; } function normalizeCompatibleRequestPayload( requestPayload: unknown, provider: LlmContextSummary["provider"], ): NormalizedPayloadCandidate | null { switch (provider) { case "openai": return normalizeOpenAiRequestPayload(requestPayload, true); case "anthropic": return normalizeAnthropicRequestPayload(requestPayload, true); case "gemini": return normalizeGeminiRequestPayload(requestPayload); } } function hasOpenAiModelPrefix(model: string | undefined): boolean { if (!model) { return false; } return /^(gpt-|chatgpt-|ft:|o[1-9]\d*(-|$))/.test(model); } function hasAnthropicModelPrefix(model: string | undefined): boolean { if (!model) { return false; } return model.startsWith("claude-"); } function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value == null || Array.isArray(value)) { return null; } return value as Record; } function asRecordArray(value: unknown): Record[] | null { if (!Array.isArray(value)) { return null; } return value.filter( (entry): entry is Record => typeof entry === "object" && entry != null && !Array.isArray(entry), ); } function asString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } function asNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; }