/**
 * Shared utilities for LLM provider adapters.
 *
 * These utilities live exactly once in this module. Each adapter
 * imports what it needs rather than reimplementing.
 *
 * Spec: P3-SUB-01 Section 9 (Shared Code to Extract)
 *
 * @connexum/ai-governance
 */
import { LlmProviderError, LlmRateLimitError, } from './index.js';
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Retry `fn` with exponential back-off, jitter, and Retry-After honour.
 * Throws the last error if all attempts are exhausted.
 */
export async function exponentialBackoffRetry(fn, opts = {}) {
    const { maxAttempts = 3, baseDelayMs = 500, maxDelayMs = 30000, backoffFactor = 2, jitterFraction = 0.25, honourRetryAfter = true, retryIf, } = opts;
    let lastError;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return await fn();
        }
        catch (err) {
            lastError = err;
            // Check if we should retry at all
            if (retryIf && !retryIf(err)) {
                throw err;
            }
            // Do not retry on the last attempt
            if (attempt === maxAttempts) {
                break;
            }
            // Rate limit with explicit Retry-After: honour it
            if (honourRetryAfter &&
                err instanceof LlmRateLimitError &&
                typeof err.retryAfterMs === 'number' &&
                err.retryAfterMs > 0) {
                await delay(Math.min(err.retryAfterMs, maxDelayMs));
                continue;
            }
            // Compute exponential back-off with jitter
            const computed = Math.min(baseDelayMs * Math.pow(backoffFactor, attempt - 1), maxDelayMs);
            const jitter = computed * jitterFraction * Math.random();
            await delay(Math.round(computed + jitter));
        }
    }
    throw lastError;
}
// ---------------------------------------------------------------------------
// accumulateStream (AC 3)
// ---------------------------------------------------------------------------
/**
 * Consume an AsyncIterable<LlmStreamChunk> and collapse it into a final
 * LlmChatResponse.
 *
 * Throws if the iterable times out (default: 60s).
 */
export async function accumulateStream(iterable, modelId, timeoutMs = 60000) {
    const parts = [];
    let finishReason = 'unknown';
    let usage;
    const toolCalls = [];
    const timer = setTimeout(() => {
        throw new Error(`Stream accumulation timed out after ${timeoutMs}ms`);
    }, timeoutMs);
    try {
        for await (const chunk of iterable) {
            if (chunk.delta) {
                parts.push(chunk.delta);
            }
            if (chunk.finishReason) {
                finishReason = chunk.finishReason;
            }
            if (chunk.usage) {
                usage = chunk.usage;
            }
        }
    }
    finally {
        clearTimeout(timer);
    }
    return {
        modelId,
        content: parts.join(''),
        finishReason,
        usage,
        toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
    };
}
/**
 * Translate common LlmToolDefinition[] to a provider wire format.
 */
export function normalizeToolUseRequest(tools, format) {
    switch (format) {
        case 'openai':
        case 'azure':
            return tools.map((t) => ({
                type: 'function',
                function: {
                    name: t.name,
                    description: t.description,
                    parameters: t.parameters,
                },
            }));
        case 'google':
            return tools.map((t) => ({
                name: t.name,
                description: t.description,
                parameters: t.parameters,
            }));
        case 'bedrock':
            return tools.map((t) => ({
                toolSpec: {
                    name: t.name,
                    description: t.description,
                    inputSchema: { json: t.parameters },
                },
            }));
        default: {
            const _exhaustive = format;
            throw new LlmProviderError({
                message: `Unknown tool wire format: ${_exhaustive}`,
                code: 'UNSUPPORTED_TOOL_FORMAT',
                providerId: 'unknown',
                adapterId: 'shared',
            });
        }
    }
}
// ---------------------------------------------------------------------------
// normalizeToolUseResponse (AC 3)
// ---------------------------------------------------------------------------
/**
 * Translate a provider tool-call response into common LlmToolCallResult[].
 */
export function normalizeToolUseResponse(raw, format) {
    if (!Array.isArray(raw))
        return [];
    switch (format) {
        case 'openai':
        case 'azure': {
            return raw.map((tc) => {
                const fn = tc['function'];
                let args = {};
                if (typeof fn?.['arguments'] === 'string') {
                    try {
                        args = JSON.parse(fn['arguments']);
                    }
                    catch {
                        args = {};
                    }
                }
                else if (typeof fn?.['arguments'] === 'object' && fn?.['arguments'] !== null) {
                    args = fn['arguments'];
                }
                return {
                    id: String(tc['id'] ?? ''),
                    name: String(fn?.['name'] ?? ''),
                    arguments: args,
                };
            });
        }
        case 'google': {
            return raw.map((fc) => ({
                id: String(fc['id'] ?? `tool_${Date.now()}`),
                name: String(fc['name'] ?? ''),
                arguments: fc['args'] ?? {},
            }));
        }
        case 'bedrock': {
            return raw.map((tu) => {
                const inner = tu['toolUse'];
                return {
                    id: String(inner?.['toolUseId'] ?? ''),
                    name: String(inner?.['name'] ?? ''),
                    arguments: inner?.['input'] ?? {},
                };
            });
        }
        default: {
            const _exhaustive = format;
            throw new LlmProviderError({
                message: `Unknown tool wire format: ${_exhaustive}`,
                code: 'UNSUPPORTED_TOOL_FORMAT',
                providerId: 'unknown',
                adapterId: 'shared',
            });
        }
    }
}
// ---------------------------------------------------------------------------
// detectUnexpectedShape (AC 3, AC 25)
// ---------------------------------------------------------------------------
/**
 * Throws LlmProviderError with code UNEXPECTED_RESPONSE_SHAPE if any
 * required key is missing from obj.
 *
 * Partial-parse attempts are prohibited -- if the shape is wrong, throw.
 */
export function detectUnexpectedShape(obj, required, providerId, adapterId) {
    if (typeof obj !== 'object' || obj === null) {
        const rawShape = JSON.stringify(obj ?? null).slice(0, 200);
        throw new LlmProviderError({
            message: `Unexpected response shape from ${providerId}: not an object`,
            code: 'UNEXPECTED_RESPONSE_SHAPE',
            providerId,
            adapterId,
            raw: rawShape,
        });
    }
    const record = obj;
    const missing = required.filter((k) => !(k in record));
    if (missing.length > 0) {
        const rawShape = JSON.stringify(obj).slice(0, 200);
        throw new LlmProviderError({
            message: `Unexpected response shape from ${providerId}: missing fields [${missing.join(', ')}]`,
            code: 'UNEXPECTED_RESPONSE_SHAPE',
            providerId,
            adapterId,
            raw: rawShape,
        });
    }
}
// ---------------------------------------------------------------------------
// emitVersionMismatchIfNeeded (AC 3, AC 26)
// ---------------------------------------------------------------------------
/**
 * Emits `llm:api-version-mismatch` WARN event when the observed provider
 * API version header differs from the adapter's pinned version.
 * Does not throw -- version mismatch is a warning, not a fatal error.
 */
export function emitVersionMismatchIfNeeded(observed, pinned, adapterId, providerId, emit) {
    if (!observed || observed === pinned)
        return;
    emit({
        type: 'llm:api-version-mismatch',
        severity: 'WARN',
        adapterId,
        providerId,
        detail: `API version mismatch: pinned=${pinned}, observed=${observed}. Re-verify adapter behaviour against new provider API version.`,
        timestamp: new Date().toISOString(),
        data: { pinned, observed },
    });
}
// ---------------------------------------------------------------------------
// parseRetryAfterMs -- extract Retry-After header into milliseconds
// ---------------------------------------------------------------------------
/**
 * Parse a Retry-After header value (seconds as integer or HTTP-date)
 * into milliseconds.
 *
 * Returns 0 if unparseable.
 */
export function parseRetryAfterMs(headerValue) {
    if (!headerValue)
        return 0;
    const asSeconds = Number(headerValue.trim());
    if (!Number.isNaN(asSeconds) && asSeconds > 0) {
        return Math.round(asSeconds * 1000);
    }
    // HTTP-date format
    const asDate = new Date(headerValue.trim()).getTime();
    if (!Number.isNaN(asDate)) {
        return Math.max(0, asDate - Date.now());
    }
    return 0;
}
// ---------------------------------------------------------------------------
// Multimodal-content narrowing helpers (F-NEW-GEMMA-VISION 2026-05-09)
// ---------------------------------------------------------------------------
/**
 * Coerce `LlmMessage.content` (string | LlmContentPart[]) into the
 * plain text string that text-only adapters need. Adapters that DO
 * NOT advertise `multimodal: true` in getCapabilities() MUST call
 * this on every message — silently dropping image parts on a vision
 * payload is a governance violation.
 *
 * Throws `LlmProviderError(UNSUPPORTED_MULTIMODAL)` when the message
 * carries any image_url part. Concatenates all `text` parts when the
 * content is array-shaped but text-only.
 *
 * F-NEW-GEMMA-VISION 2026-05-09. The widening of `LlmMessage.content`
 * to `string | LlmContentPart[]` is type-only; existing string-content
 * call sites keep working unchanged.
 */
export function coerceTextContent(message, ctx) {
    if (typeof message.content === 'string')
        return message.content;
    const parts = message.content;
    const hasImage = parts.some((p) => p && p.type === 'image_url');
    if (hasImage) {
        throw new LlmProviderError({
            message: `${ctx.adapterId}: multimodal (image_url) content is not supported by ` +
                `this adapter. Use a vision-capable adapter (e.g. GoogleAdapter with ` +
                `paligemma-* / gemini-1.5-* / gemini-2.x, or OpenAIAdapter with ` +
                `gpt-4o / gpt-4-vision). Silently dropping image parts is a ` +
                `governance violation (F-NEW-GEMMA-VISION).`,
            code: 'UNSUPPORTED_MULTIMODAL',
            providerId: ctx.providerId,
            adapterId: ctx.adapterId,
        });
    }
    // Array content but text-only — concatenate text parts.
    return parts
        .filter((p) => p && p.type === 'text')
        .map((p) => p.text)
        .join('');
}
/**
 * Returns true if any message in the list carries an image_url part.
 * Adapters use this to decide whether to fail-fast at request entry
 * vs let the per-message coerce path handle it.
 *
 * F-NEW-GEMMA-VISION 2026-05-09.
 */
export function requestHasImageContent(messages) {
    for (const m of messages) {
        if (Array.isArray(m.content)) {
            for (const p of m.content) {
                if (p && p.type === 'image_url')
                    return true;
            }
        }
    }
    return false;
}
//# sourceMappingURL=shared.js.map