/**
 * DeepSeek SSE Transform — extracts `reasoning_content` from DeepSeek-R1 streaming responses.
 *
 * **Why this exists:**
 * DeepSeek-R1 (`deepseek-reasoner`) returns chain-of-thought as `delta.reasoning_content`
 * inside each SSE chunk. The Vercel AI SDK's @ai-sdk/openai chat-completions parser
 * (3.0.41) accumulates `reasoning_tokens` into `usage.reasoningTokens` (correct billing
 * surface) but **does not** map `reasoning_content` text to any content-part or
 * `reasoningText` field. Result: users pay for reasoning tokens but the reasoning
 * text is silently dropped.
 *
 * **What this module does:**
 * - Pure stream transformer: takes a DeepSeek SSE response body, tees it,
 *   passes one branch through unchanged (for AI SDK to consume normally),
 *   and parses the other branch to extract reasoning_content into an
 *   accumulator string.
 * - Zero dependency on AI SDK internals — operates only on the raw SSE byte
 *   stream that AI SDK is about to consume.
 *
 * **Architectural note (Sam memo reconciliation):**
 * Earlier scope memo referenced wrapping a "PR #425 translator output" layer.
 * Verified: PR #425 is the frontend Settings UI fix; no Crewly-owned translator
 * exists. The only seam Crewly owns between DeepSeek and the AI SDK is the
 * `createOpenAI({ fetch })` upstream hook in model-manager.ts. This module is
 * the body of that hook.
 *
 * **DeepClaude pattern reference (read-only):**
 * Reasoning-content extraction approach borrowed conceptually from public
 * DeepClaude pattern (HTTP proxy that splits R1 reasoning from final answer).
 * **No code, no fork, no dependency** — pattern reference only, per Anthropic
 * ToS guardrail flagged by Arch.
 *
 * @module services/agent/crewly-agent/deepseek-sse-transform
 */
/**
 * Parse a single SSE event-block (one or more `data:` lines + a blank line).
 *
 * Returns the accumulated reasoning_content string from this block, or
 * empty string if the block had no reasoning content. Returns `null` if
 * the block is the `[DONE]` sentinel.
 *
 * @param block - One SSE event block (between blank-line delimiters)
 * @returns reasoning_content string, or `null` if `[DONE]`
 */
export function parseSseBlock(block) {
    const lines = block.split('\n');
    let reasoning = '';
    for (const line of lines) {
        if (!line.startsWith('data:'))
            continue;
        const payload = line.slice(5).trim();
        if (!payload)
            continue;
        if (payload === '[DONE]')
            return null;
        try {
            const chunk = JSON.parse(payload);
            const r = chunk.choices?.[0]?.delta?.reasoning_content;
            if (typeof r === 'string') {
                reasoning += r;
            }
        }
        catch {
            // Malformed JSON — skip silently; AI SDK consumer will surface
            // any real error itself when it parses the same chunk.
        }
    }
    return reasoning;
}
/**
 * Tee a DeepSeek SSE response body and parse one branch for reasoning_content
 * while passing the other branch through to the AI SDK consumer unchanged.
 *
 * The parser runs as a fire-and-forget background reader on the cloned stream.
 * It accumulates reasoning text into an internal buffer that the caller can
 * read via `getReasoning()` after the consumer has drained the passthrough.
 *
 * **Stream safety:**
 * - The tee is symmetric: backpressure on the consumer branch does not
 *   stall the parser branch (and vice versa) thanks to ReadableStream.tee()
 *   internal buffering.
 * - Parser errors are caught and logged but never propagated to the consumer.
 *
 * @param body - Raw SSE response body from DeepSeek
 * @returns Object with passthrough body and reasoning accumulator
 */
export function teeAndParse(body) {
    const [consumerBranch, parserBranch] = body.tee();
    let reasoning = '';
    let drained = false;
    // Background reader: drain parserBranch, parse SSE, accumulate reasoning.
    // Errors are swallowed — consumer branch is independent and unaffected.
    void (async () => {
        const reader = parserBranch.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                buffer += decoder.decode(value, { stream: true });
                // SSE event blocks are delimited by blank lines (\n\n).
                let blankIdx;
                while ((blankIdx = buffer.indexOf('\n\n')) >= 0) {
                    const block = buffer.slice(0, blankIdx);
                    buffer = buffer.slice(blankIdx + 2);
                    const result = parseSseBlock(block);
                    if (result === null) {
                        drained = true;
                    }
                    else if (result) {
                        reasoning += result;
                    }
                }
            }
            // Flush trailing partial block (if SSE source ended mid-block — defensive)
            if (buffer.trim()) {
                const result = parseSseBlock(buffer);
                if (result && result !== null)
                    reasoning += result;
            }
        }
        catch (err) {
            // Parser failures must not break consumer flow — log and exit.
            // eslint-disable-next-line no-console
            console.warn('[DeepSeek SSE Transform] parser branch error (consumer unaffected):', err);
        }
        finally {
            drained = true;
            try {
                reader.releaseLock();
            }
            catch {
                /* already released */
            }
        }
    })();
    return {
        passthroughBody: consumerBranch,
        getReasoning: () => reasoning,
        isDrained: () => drained,
    };
}
//# sourceMappingURL=deepseek-sse-transform.js.map