From b9c7fd879fc63cd73415a1a2b193d729447a36cd Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 28 Mar 2026 23:09:31 -0300 Subject: [PATCH] fix(core): resolve routing schemas, CLI streaming leaks, and thinking tag extraction --- .npmignore | 9 ++++++++ open-sse/handlers/chatCore.ts | 9 ++++---- open-sse/handlers/responseSanitizer.ts | 9 ++++---- open-sse/utils/stream.ts | 30 ++++++++++++++++++++------ src/shared/validation/schemas.ts | 7 +++++- 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/.npmignore b/.npmignore index c5ae66e7..c11b218d 100644 --- a/.npmignore +++ b/.npmignore @@ -47,3 +47,12 @@ AGENTS.md # Build artifacts (pre-built goes inside app/) .next/ node_modules/ + +# Ignore large binary files and other build directories +*.tgz +*.AppImage +*.deb +*.rpm +electron/ +app/electron/ +app/vscode-extension/ diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 2a3ee9c0..4cdbf8d1 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -531,10 +531,10 @@ export async function handleChatCore({ connectionId, duration: Date.now() - startTime, tokens: tokens || {}, - requestBody: attachLogMeta(body, { + requestBody: attachLogMeta((body as Record) ?? undefined, { claudePromptCache: claudeCacheMeta, }), - responseBody: attachLogMeta(responseBody ?? undefined, { + responseBody: attachLogMeta((responseBody as Record) ?? undefined, { claudePromptCache: claudeCacheMeta ? { applied: claudeCacheMeta.applied, @@ -1464,8 +1464,9 @@ export async function handleChatCore({ // Sanitize response for OpenAI SDK compatibility // Strips non-standard fields (x_groq, usage_breakdown, service_tier, etc.) - // Extracts tags into reasoning_content - if (sourceFormat === FORMATS.OPENAI) { + // Extracts and tags into reasoning_content + // Target format determines output shape. If we are outputting OpenAI shape or pseudo-OpenAI shape, sanitize. + if (targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES) { translatedResponse = sanitizeOpenAIResponse(translatedResponse); } diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts index cb56c5ef..24e36148 100644 --- a/open-sse/handlers/responseSanitizer.ts +++ b/open-sse/handlers/responseSanitizer.ts @@ -32,13 +32,12 @@ function toNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } -// ── Think tag regex ──────────────────────────────────────────────────────── -// Matches ... blocks (greedy, dotAll) -const THINK_TAG_REGEX = /([\s\S]*?)<\/think>/gi; +// Matches ... blocks and ... (greedy, dotAll) +const THINK_TAG_REGEX = /<(?:think|thinking)>([\s\S]*?)<\/(?:think|thinking)>/gi; -// #638: Collapse runs of 3+ consecutive newlines into \n\n +// #638, #727: Collapse runs of 2+ consecutive newlines into \n\n // Tool call responses from thinking models often accumulate excessive newlines -const EXCESSIVE_NEWLINES = /\n{3,}/g; +const EXCESSIVE_NEWLINES = /\n{2,}/g; function collapseExcessiveNewlines(text: string): string { return text.replace(EXCESSIVE_NEWLINES, "\n\n"); } diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 8a2fe511..b8c78803 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -528,31 +528,47 @@ export function createSSEStream(options: StreamOptions = {}) { // Content for call log is accumulated only from parsed (above) to avoid double-counting; // do not add again from item here. + // #723, #727: Sanitize intermediate stream chunks if target is OpenAI format loop + let itemSanitized: Record = item; + if (targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES) { + itemSanitized = sanitizeStreamingChunk(itemSanitized) as Record; + + // Extract reasoning tags from content if translation generated them + const delta = itemSanitized?.choices?.[0]?.delta; + if (delta?.content && typeof delta.content === "string") { + const { content, thinking } = extractThinkingFromContent(delta.content); + delta.content = content; + if (thinking && !delta.reasoning_content) { + delta.reasoning_content = thinking; + } + } + } + // Filter empty chunks - if (!hasValuableContent(item, sourceFormat)) { + if (!hasValuableContent(itemSanitized, sourceFormat)) { continue; // Skip this empty chunk } // Inject estimated usage if finish chunk has no valid usage const isFinishChunk = - item.type === "message_delta" || item.choices?.[0]?.finish_reason; + itemSanitized.type === "message_delta" || itemSanitized.choices?.[0]?.finish_reason; if ( state.finishReason && isFinishChunk && - !hasValidUsage(item.usage) && + !hasValidUsage(itemSanitized.usage) && totalContentLength > 0 ) { const estimated = estimateUsage(body, totalContentLength, sourceFormat); - item.usage = filterUsageForFormat(estimated, sourceFormat); // Filter + already has buffer + itemSanitized.usage = filterUsageForFormat(estimated, sourceFormat); // Filter + already has buffer state.usage = estimated; } else if (state.finishReason && isFinishChunk && state.usage) { // Add buffer and filter usage for client (but keep original in state.usage for logging) const buffered = addBufferToUsage(state.usage); - item.usage = filterUsageForFormat(buffered, sourceFormat); + itemSanitized.usage = filterUsageForFormat(buffered, sourceFormat); } - const output = formatSSE(item, sourceFormat); - clientPayloadCollector.push(item); + const output = formatSSE(itemSanitized, sourceFormat); + clientPayloadCollector.push(itemSanitized); reqLogger?.appendConvertedChunk?.(output); controller.enqueue(encoder.encode(output)); } diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index f4d58c66..0c32f398 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -78,6 +78,9 @@ const comboStrategySchema = z.enum([ "cost-optimized", "strict-random", "auto", + "fill-first", + // #729 schema fixes for combo edit/save + "p2c", ]); const comboRuntimeConfigSchema = z @@ -884,6 +887,7 @@ export const updateComboSchema = z system_message: z.string().max(50000).optional(), tool_filter_regex: z.string().max(1000).optional(), context_cache_protection: z.boolean().optional(), + context_length: z.number().int().min(1000).max(2000000).optional(), }) .superRefine((value, ctx) => { if ( @@ -895,7 +899,8 @@ export const updateComboSchema = z value.allowedProviders === undefined && value.system_message === undefined && value.tool_filter_regex === undefined && - value.context_cache_protection === undefined + value.context_cache_protection === undefined && + value.context_length === undefined ) { ctx.addIssue({ code: z.ZodIssueCode.custom,