/** * Keeping a model's reasoning out of its answer, at the provider boundary. * * A cerebras reply put reasoning into an ntfy notification whose surface * policy is `reasoningVisibility: 'suppress'`. The suppression was never at * fault, the reasoning never reached the reasoning channel at all, so nothing * downstream could tell it apart from the answer. It reached the transcript, * the session export and every channel body as plain assistant text. * * Reasoning arrives in two shapes, and BOTH were landing in `content`: * * 1. A structured field (`reasoning`, `reasoning_content`). * openai-stream-delta.ts knows how to route these, but only when its * `allowReasoning` option is set, and openai-compat derived that from the * provider's `reasoningFormat`, which is a REQUEST-side setting naming * which reasoning PARAMETER an endpoint accepts. Cerebras, groq and mistral * are all registered `reasoningFormat: 'none'`, which told the extractor to * FOLD a returned reasoning field into `content`. Cerebras returns * reasoning on exactly that field, so its chain-of-thought became ordinary * answer text, interleaved with the real answer. What a response CARRIES is * not a function of what the request ASKED FOR: a reasoning field is * reasoning on every endpoint, so openai-compat now always classifies it. * * 2. A tag inside the content stream (``), which some models * emit with no structured field at all. {@link splitInlineReasoning} and * {@link InlineReasoningStreamSplitter} below handle that shape. * * Both belong here rather than at a render site: the wire format is the * provider's concern, and splitting once means every consumer, TUI * transcript, webui, session export, channel surfaces, gets the same correct * content/reasoning split and applies its own visibility policy to it. A * filter at any single render site would fix that site and leave every other * one reading reasoning as the answer. * * One invariant governs all of it: the split must never EMPTY a reply. A model * that writes nothing outside its reasoning has no answer beside it, so the * reasoning IS the answer, see the floor in openai-compat.ts, which is what * the old folding behaviour was really protecting. */ /** * Tag names recognised as inline reasoning wrappers. * * Deliberately short. Each entry is a name that models emit as a literal * XML-ish wrapper around chain-of-thought; a name that could plausibly appear * as real markup in an answer does not belong here. */ export declare const INLINE_REASONING_TAGS: readonly string[]; export interface InlineReasoningSplit { /** The answer, with every reasoning span removed. */ readonly content: string; /** The reasoning spans, joined in the order they appeared. */ readonly reasoning: string; } /** * Split a COMPLETE assistant message into answer and inline reasoning. * * An unterminated opening tag (the model was cut off mid-thought, or the * endpoint dropped the closing tag) takes everything after it as reasoning * rather than leaving a dangling `` in the answer. */ export declare function splitInlineReasoning(text: string): InlineReasoningSplit; /** * Streaming form of {@link splitInlineReasoning}. * * Applied to the delta loop rather than only to the assembled response so the * two agree: splitting only at the end would stream the reasoning to the TUI * as answer text and then retract it when the turn completed. Text that could * still turn out to be the start of a delimiter is held back until the next * chunk resolves it, so a tag split across chunk boundaries is not missed and * no partial tag is ever emitted. * * `flush()` must be called once the stream ends to release whatever is held. */ export declare class InlineReasoningStreamSplitter { private buffer; private openTag; push(chunk: string): InlineReasoningSplit; /** Release the held-back tail. Inside an unterminated tag it is reasoning. */ flush(): InlineReasoningSplit; } /** What a stream-delta extractor produced for one chunk. */ export interface StreamTextFragments { readonly content: readonly string[]; readonly reasoning: readonly string[]; } /** Forwards a classified fragment to the caller's live delta handler. */ export type StreamTextEmit = (delta: { content?: string; reasoning?: string; }) => void; /** * Accumulates one streamed turn, keeping reasoning out of the answer. * * Owns both shapes reasoning arrives in, a structured `reasoning` / * `reasoning_content` field, and a tag inside the content stream, and keeps * the running answer and the running reasoning apart, forwarding each fragment * as it is classified so a live view is never shown reasoning as answer text * and then made to retract it. */ export declare class StreamTextAccumulator { private readonly splitter; private contentText; private reasoningText; push(fragments: StreamTextFragments, emit?: StreamTextEmit): void; /** * Release whatever the splitter still holds back and report the turn. * * `content` is empty when the model wrote nothing outside its reasoning, * the caller decides what that means, since an empty answer is normal on a * tool-call turn and a lost reply on any other. */ finish(emit?: StreamTextEmit): InlineReasoningSplit; private take; } //# sourceMappingURL=inline-reasoning.d.ts.map