/**
 * Responses API adapter for the OpenAI model provider.
 *
 * Built-in tool support status:
 * | Tool              | Support                                                  |
 * |-------------------|----------------------------------------------------------|
 * | web_search        | Full: includes URL citations                             |
 * | file_search       | Partial: works but file citation annotations not emitted |
 * | code_interpreter  | Partial: works but executed code/stdout not surfaced     |
 * | mcp               | Partial: works but approval flow not supported           |
 * | shell             | Partial: container mode only                             |
 * | image_generation  | Not supported                                            |
 *
 * @internal
 */
import { encodeBase64 } from '../../types/media.js';
import { toMimeType } from '../../mime.js';
import { logger } from '../../logging/logger.js';
import { MODEL_DEFAULTS } from '../defaults.js';
import { formatImageDataUrl, warnManagedParams as warnManagedParamsShared } from './formatting.js';
export const DEFAULT_RESPONSES_MODEL_ID = MODEL_DEFAULTS.openai.modelId;
const MANAGED_PARAMS = new Set(['model', 'input', 'stream', 'store']);
/**
 * Logs a warning for each responses-managed key present in `params`.
 *
 * @internal
 */
export function warnManagedParams(params) {
    warnManagedParamsShared(params, MANAGED_PARAMS);
}
/**
 * Builds a Responses API streaming request body.
 *
 * @internal
 */
export function formatResponsesRequest(config, messages, options, stateful) {
    const input = formatResponsesMessages(messages);
    // User `params` are spread first so provider-managed fields (asserted
    // required by `ResponseCreateParamsStreaming` below) always win. The
    // managed-params warning fires at config time to surface the collision.
    const request = {
        ...(config.params ?? {}),
        model: config.modelId ?? DEFAULT_RESPONSES_MODEL_ID,
        input,
        stream: true,
        store: stateful,
    };
    if (stateful) {
        const responseId = options?.modelState?.get('responseId');
        if (responseId) {
            request.previous_response_id = responseId;
        }
    }
    if (options?.systemPrompt !== undefined) {
        if (typeof options.systemPrompt === 'string') {
            request.instructions = options.systemPrompt;
        }
        else if (Array.isArray(options.systemPrompt)) {
            const texts = [];
            for (const block of options.systemPrompt) {
                if (block.type === 'textBlock') {
                    texts.push(block.text);
                }
            }
            if (texts.length > 0) {
                request.instructions = texts.join('');
            }
        }
    }
    if (options?.toolSpecs && options.toolSpecs.length > 0) {
        const existingTools = request.tools ?? [];
        request.tools = [
            ...existingTools,
            ...options.toolSpecs.map((spec) => ({
                type: 'function',
                name: spec.name,
                description: spec.description ?? '',
                parameters: (spec.inputSchema ?? {}),
                // `null` defers to the OpenAI server default. The SDK's typed
                // contract requires a value; omitting it (as the Python SDK does)
                // is not an option here.
                strict: null,
            })),
        ];
        if (options.toolChoice) {
            if ('auto' in options.toolChoice) {
                request.tool_choice = 'auto';
            }
            else if ('any' in options.toolChoice) {
                request.tool_choice = 'required';
            }
            else if ('tool' in options.toolChoice) {
                request.tool_choice = { type: 'function', name: options.toolChoice.tool.name };
            }
        }
    }
    if (config.temperature !== undefined)
        request.temperature = config.temperature;
    if (config.maxTokens !== undefined)
        request.max_output_tokens = config.maxTokens;
    if (config.topP !== undefined)
        request.top_p = config.topP;
    return request;
}
/**
 * Formats SDK messages into Responses API input items.
 *
 * Per message, content blocks are split into three buckets:
 * - Text/media → grouped in `{ role, content: [...] }`
 * - Tool calls → separate `{ type: 'function_call', ... }` items
 * - Tool results → separate `{ type: 'function_call_output', ... }` items
 */
function formatResponsesMessages(messages) {
    const input = [];
    for (const message of messages) {
        const role = message.role === 'assistant' ? 'assistant' : 'user';
        const contentItems = [];
        const toolCallItems = [];
        const toolResultItems = [];
        for (const block of message.content) {
            switch (block.type) {
                case 'textBlock': {
                    if (role === 'user') {
                        contentItems.push({ type: 'input_text', text: block.text });
                    }
                    else {
                        contentItems.push({ type: 'output_text', text: block.text });
                    }
                    break;
                }
                case 'imageBlock': {
                    const formatted = formatImageInput(block);
                    if (formatted)
                        contentItems.push(formatted);
                    break;
                }
                case 'documentBlock': {
                    const formatted = formatDocumentInput(block);
                    if (formatted)
                        contentItems.push(formatted);
                    break;
                }
                case 'citationsBlock': {
                    const citBlock = block;
                    for (const c of citBlock.content) {
                        contentItems.push({ type: 'output_text', text: c.text });
                    }
                    break;
                }
                case 'toolUseBlock': {
                    const toolBlock = block;
                    const call = {
                        type: 'function_call',
                        call_id: toolBlock.toolUseId,
                        name: toolBlock.name,
                        arguments: JSON.stringify(toolBlock.input),
                    };
                    toolCallItems.push(call);
                    break;
                }
                case 'toolResultBlock': {
                    const resultBlock = block;
                    const result = {
                        type: 'function_call_output',
                        call_id: resultBlock.toolUseId,
                        output: formatToolResultOutput(resultBlock),
                    };
                    toolResultItems.push(result);
                    break;
                }
                case 'reasoningBlock': {
                    logger.warn('block_type=<reasoningBlock> | reasoning content is not yet supported in multi-turn conversations with the responses api');
                    break;
                }
                default: {
                    logger.warn(`block_type=<${block.type}> | unsupported content type in responses api message formatting | skipping`);
                }
            }
        }
        // Cast is needed because assistant messages here use `output_text` content
        // blocks, which the SDK's input types model as `ResponseOutputMessage` —
        // a response-shaped type that requires `id`/`status`/`annotations`. The API
        // accepts these fields as omitted on input, but the SDK types don't reflect that.
        if (contentItems.length > 0) {
            input.push({
                role,
                content: contentItems,
            });
        }
        input.push(...toolCallItems);
        input.push(...toolResultItems);
    }
    return input;
}
/**
 * Builds a Responses API `function_call_output.output` value from a SDK
 * `toolResultBlock`. Returns a plain string for text-only results (joined with
 * newlines) or the content-item array shape when the result carries image or
 * document data.
 */
function formatToolResultOutput(resultBlock) {
    const parts = [];
    const texts = [];
    let hasMedia = false;
    for (const c of resultBlock.content) {
        switch (c.type) {
            case 'textBlock':
                texts.push(c.text);
                parts.push({ type: 'input_text', text: c.text });
                break;
            case 'jsonBlock': {
                const jsonBlock = c;
                let text;
                try {
                    text = JSON.stringify(jsonBlock.json);
                }
                catch {
                    text = '[JSON serialization error]';
                }
                texts.push(text);
                parts.push({ type: 'input_text', text });
                break;
            }
            case 'imageBlock': {
                const url = formatImageDataUrl(c);
                if (url) {
                    hasMedia = true;
                    parts.push({ type: 'input_image', image_url: url });
                }
                break;
            }
            case 'documentBlock': {
                const docBlock = c;
                if (docBlock.source.type === 'documentSourceBytes') {
                    const base64 = encodeBase64(docBlock.source.bytes);
                    const mimeType = toMimeType(docBlock.format) || `application/${docBlock.format}`;
                    hasMedia = true;
                    parts.push({
                        type: 'input_file',
                        file_data: `data:${mimeType};base64,${base64}`,
                        filename: docBlock.name,
                    });
                }
                else {
                    logger.warn(`source_type=<${docBlock.source.type}> | only byte source documents supported in responses api tool results`);
                }
                break;
            }
            default:
                logger.warn(`block_type=<${c.type}> | unsupported tool result content type for responses api`);
        }
    }
    if (hasMedia)
        return parts;
    // Text-only: collapse to a single string to match the API's simpler shape.
    const text = texts.join('\n');
    if (resultBlock.status === 'error') {
        return `[ERROR] ${text}`;
    }
    return text;
}
function formatImageInput(imageBlock) {
    const url = formatImageDataUrl(imageBlock);
    if (!url)
        return undefined;
    return { type: 'input_image', image_url: url };
}
function formatDocumentInput(docBlock) {
    if (docBlock.source.type === 'documentSourceBytes') {
        const base64 = encodeBase64(docBlock.source.bytes);
        const mimeType = toMimeType(docBlock.format) || `application/${docBlock.format}`;
        return {
            type: 'input_file',
            file_data: `data:${mimeType};base64,${base64}`,
            filename: docBlock.name,
        };
    }
    logger.warn(`source_type=<${docBlock.source.type}> | only byte source documents supported in responses api`);
    return undefined;
}
/**
 * Creates fresh stream state for a new Responses API stream.
 *
 * @internal
 */
export function createResponsesStreamState() {
    return {
        dataType: null,
        toolCalls: new Map(),
        finalUsage: null,
        stopReason: 'endTurn',
    };
}
/**
 * Maps a Responses API `usage` object to the SDK's usage shape, including
 * prompt-cache reads. The Responses API reports cache hits via
 * `usage.input_tokens_details.cached_tokens`; surfacing it as
 * `cacheReadInputTokens` keeps the Responses path consistent with the Bedrock,
 * Anthropic, and Vercel model adapters (and lets `telemetry/tracer.ts` emit
 * `gen_ai.usage.cache_read_input_tokens`).
 *
 * @internal
 */
function mapResponsesUsage(usage) {
    const mapped = {
        inputTokens: usage.input_tokens,
        outputTokens: usage.output_tokens,
        totalTokens: usage.total_tokens,
    };
    const cached = usage.input_tokens_details?.cached_tokens;
    if (typeof cached === 'number' && cached > 0) {
        mapped.cacheReadInputTokens = cached;
    }
    return mapped;
}
/**
 * Maps a single Responses API stream event to zero or more SDK events. Mutates
 * `state` and, when `stateful` is `true`, writes `responseId` into `modelState`.
 *
 * @internal
 */
export function mapResponsesEventToSDK(event, state, stateful, modelState) {
    const events = [];
    switch (event.type) {
        case 'response.created': {
            if (stateful && modelState) {
                modelState.set('responseId', event.response.id);
            }
            events.push({ type: 'modelMessageStartEvent', role: 'assistant' });
            break;
        }
        case 'response.output_text.delta': {
            events.push(...switchContent('text', state.dataType));
            state.dataType = 'text';
            events.push({
                type: 'modelContentBlockDeltaEvent',
                delta: { type: 'textDelta', text: event.delta },
            });
            break;
        }
        case 'response.reasoning_text.delta':
        case 'response.reasoning_summary_text.delta': {
            events.push(...switchContent('reasoning', state.dataType));
            state.dataType = 'reasoning';
            events.push({
                type: 'modelContentBlockDeltaEvent',
                delta: { type: 'reasoningContentDelta', text: event.delta },
            });
            break;
        }
        case 'response.output_text.annotation.added': {
            // The SDK types `event.annotation` as `unknown` and doesn't export a
            // named annotation union, so we narrow structurally on the fields we use.
            const annotation = event.annotation;
            if (annotation.type === 'url_citation') {
                // Close the in-flight text block before the citation delta.
                // model.ts finalization picks ONE block kind per open block
                // (citations wins over text), so text + citation in the same
                // block drops the text on stop. Switching here forces a
                // separate CitationsBlock, and the next text delta will open
                // a fresh TextBlock.
                events.push(...switchContent('citations', state.dataType));
                state.dataType = 'citations';
                events.push({
                    type: 'modelContentBlockDeltaEvent',
                    delta: {
                        type: 'citationsDelta',
                        citations: [
                            {
                                location: {
                                    type: 'web',
                                    url: annotation.url ?? '',
                                },
                                source: annotation.url ?? '',
                                sourceContent: [],
                                title: annotation.title ?? '',
                            },
                        ],
                        content: [{ text: annotation.cited_text ?? '' }],
                    },
                });
            }
            else {
                logger.warn(`annotation_type=<${annotation.type}> | unsupported annotation type in responses api`);
            }
            break;
        }
        case 'response.output_item.added': {
            if (event.item.type === 'function_call') {
                // `id` is optional in the SDK type but load-bearing here: it keys
                // subsequent argument delta/done events. Skip rather than collapse
                // to an empty string, which would let distinct calls share a key.
                const { id: itemId, call_id: callId, name } = event.item;
                if (!itemId) {
                    logger.warn(`call_id=<${callId}> name=<${name}> | function_call event missing item id — skipping`);
                    break;
                }
                state.toolCalls.set(itemId, { name, arguments: '', callId, itemId });
            }
            break;
        }
        case 'response.function_call_arguments.delta': {
            const tc = state.toolCalls.get(event.item_id);
            if (tc) {
                tc.arguments += event.delta;
            }
            break;
        }
        case 'response.function_call_arguments.done': {
            const tc = state.toolCalls.get(event.item_id);
            if (tc) {
                tc.arguments = event.arguments;
            }
            break;
        }
        case 'response.incomplete': {
            const resp = event.response;
            if (resp.usage) {
                state.finalUsage = mapResponsesUsage(resp.usage);
            }
            if (resp.incomplete_details?.reason === 'max_output_tokens') {
                state.stopReason = 'maxTokens';
            }
            break;
        }
        case 'response.completed': {
            const resp = event.response;
            if (resp.usage) {
                state.finalUsage = mapResponsesUsage(resp.usage);
            }
            break;
        }
        default:
            break;
    }
    return events;
}
/**
 * Emits the terminal events for a Responses API stream: closes any open content
 * block, flushes accumulated tool calls, emits usage metadata, and finishes
 * with `modelMessageStopEvent`.
 *
 * @internal
 */
export function finalizeResponsesStream(state) {
    const events = [];
    if (state.dataType !== null) {
        events.push({ type: 'modelContentBlockStopEvent' });
    }
    for (const [, tc] of state.toolCalls) {
        events.push({
            type: 'modelContentBlockStartEvent',
            start: { type: 'toolUseStart', name: tc.name, toolUseId: tc.callId },
        });
        events.push({
            type: 'modelContentBlockDeltaEvent',
            delta: { type: 'toolUseInputDelta', input: tc.arguments },
        });
        events.push({ type: 'modelContentBlockStopEvent' });
    }
    let stopReason = state.stopReason;
    if (state.toolCalls.size > 0) {
        stopReason = 'toolUse';
    }
    if (state.finalUsage) {
        events.push({ type: 'modelMetadataEvent', usage: state.finalUsage });
    }
    events.push({ type: 'modelMessageStopEvent', stopReason });
    return events;
}
function switchContent(newType, prevType) {
    const events = [];
    if (newType !== prevType) {
        if (prevType !== null) {
            events.push({ type: 'modelContentBlockStopEvent' });
        }
        events.push({ type: 'modelContentBlockStartEvent' });
    }
    return events;
}
//# sourceMappingURL=responses-adapter.js.map