Files
OmniRoute/open-sse/executors/cursor/composer.ts
Diego Rodrigues de Sa e Souza 4eaa2f4c65 refactor(executors): extract pure prompt + composer helpers from cursor (#5960)
Extract two pure clusters from the cursor executor into sibling leaves:
- cursor/prompt.ts: isRecordLike + toolChoiceDirectiveLine + buildCursorOutputConstraints
- cursor/composer.ts: composer thinking-as-content decoding (isComposerModel,
  visibleComposerContentFromThinking, composerReasoningRemainder + markers)

Host imports both back for internal use and re-exports the 3 composer helpers for
external importers (tests). Host 1576 -> 1451 LOC. Byte-identical bodies (verbatim
multiset prompt 65/65, composer 32/32), leaves have zero imports (no cycle). Adds a
split-guard; consumer tests stay green (cursor-composer-thinking, cursor-streaming,
cursor-agent-tool-calls, translator-openai-to-cursor, cursor-agent-system-prompt).
2026-07-02 18:41:06 -03:00

54 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Composer thinking-as-content decoding for the Cursor executor (verbatim, no host imports).
// ─── Composer thinking-as-content decoding ─────────────────────────────────
//
// The Cursor `composer-*` family encodes its visible reply inside the
// `thinking` field, marked off from the (private) chain-of-thought by a
// final `</think>` sentinel. Everything AFTER the last `</think>` is the
// user-facing reply; the prefix must stay hidden.
//
// Ported from decolua/9router#1310 by Noé Rivera. Same algorithm, adapted
// to OmniRoute's StreamCtx-based pipeline so streaming + non-streaming
// share the accumulation path.
const COMPOSER_THINK_END = "</think>";
export function isComposerModel(model: string | undefined | null): boolean {
const id = String(model ?? "")
.split("/")
.pop();
return /^composer(?:-|$)/i.test(id ?? "");
}
// Composer's protobuf sometimes wraps the visible suffix in sentinel tags:
// `<final>` (full-width pipes) or `<|final|>` (ASCII), optionally closed
// with a matching `</final>` / `<|/final|>`. These are protocol-internal
// and must never leak to OpenAI-compatible clients (decolua/9router#1316).
const COMPOSER_OPEN_MARKER = /^\s*<[|]\s*final\s*[|]>\s*/i;
const COMPOSER_CLOSE_MARKER = /\s*<[|]\s*\/\s*final\s*[|]>\s*$/i;
const COMPOSER_PARTIAL_OPEN = /^\s*<(?![|/])/;
const COMPOSER_PARTIAL_OPEN_PIPE = /^\s*<[|][^>]*$/;
export function visibleComposerContentFromThinking(thinking: string): string {
if (!thinking) return "";
const endIdx = thinking.lastIndexOf(COMPOSER_THINK_END);
if (endIdx < 0) return "";
let visible = thinking.slice(endIdx + COMPOSER_THINK_END.length).trimStart();
if (COMPOSER_OPEN_MARKER.test(visible)) {
visible = visible.replace(COMPOSER_OPEN_MARKER, "");
} else if (COMPOSER_PARTIAL_OPEN.test(visible) || COMPOSER_PARTIAL_OPEN_PIPE.test(visible)) {
// A streamed chunk delivered only a partial opening marker (e.g. `<` or
// `<fin`). Hold back everything until more data arrives so the marker
// fragment never leaks as content.
return "";
}
return visible.replace(COMPOSER_CLOSE_MARKER, "").trim();
}
export function composerReasoningRemainder(thinking: string): string {
if (!thinking) return "";
const endIdx = thinking.lastIndexOf(COMPOSER_THINK_END);
if (endIdx < 0) return thinking;
return thinking.slice(0, endIdx);
}