fix(core): resolve routing schemas, CLI streaming leaks, and thinking tag extraction

This commit is contained in:
diegosouzapw
2026-03-28 23:09:31 -03:00
parent 2fc4c7ea33
commit b9c7fd879f
5 changed files with 47 additions and 17 deletions
+9
View File
@@ -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/
+5 -4
View File
@@ -531,10 +531,10 @@ export async function handleChatCore({
connectionId,
duration: Date.now() - startTime,
tokens: tokens || {},
requestBody: attachLogMeta(body, {
requestBody: attachLogMeta((body as Record<string, unknown>) ?? undefined, {
claudePromptCache: claudeCacheMeta,
}),
responseBody: attachLogMeta(responseBody ?? undefined, {
responseBody: attachLogMeta((responseBody as Record<string, unknown>) ?? 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 <think> tags into reasoning_content
if (sourceFormat === FORMATS.OPENAI) {
// Extracts <think> and <thinking> 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);
}
+4 -5
View File
@@ -32,13 +32,12 @@ function toNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
// ── Think tag regex ────────────────────────────────────────────────────────
// Matches <think>...</think> blocks (greedy, dotAll)
const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/gi;
// Matches <think>...</think> blocks and <thinking>...</thinking> (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");
}
+23 -7
View File
@@ -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<string, unknown> = item;
if (targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES) {
itemSanitized = sanitizeStreamingChunk(itemSanitized) as Record<string, unknown>;
// 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));
}
+6 -1
View File
@@ -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,