From 373b10ebb52115bd34629bc4c2ba5a95d9788e66 Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 13 Mar 2026 09:41:40 +0700 Subject: [PATCH] feat(chat): Enhance bypass handling and introduce CC filter naming feature Fix : Ollam Provider response --- open-sse/handlers/chatCore.js | 6 +-- open-sse/utils/bypassHandler.js | 42 +++++++++++++++---- open-sse/utils/stream.js | 4 +- .../cli-tools/components/ClaudeToolCard.js | 32 +++++++++++++- src/shared/components/Tooltip.js | 19 +++++++++ src/shared/components/index.js | 1 + src/sse/handlers/chat.js | 2 + 7 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 src/shared/components/Tooltip.js diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 43f766e..16f01c0 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -23,14 +23,14 @@ import { handleStreamingResponse, buildOnStreamComplete } from "./chatCore/strea * @param {object} options.credentials - Provider credentials * @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses") */ -export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, sourceFormatOverride }) { +export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, sourceFormatOverride }) { const { provider, model } = modelInfo; const requestStartTime = Date.now(); const sourceFormat = sourceFormatOverride || detectFormat(body); - // Check for bypass patterns (warmup, skip) - const bypassResponse = handleBypassRequest(body, model, userAgent); + // Check for bypass patterns (warmup, skip, cc naming) + const bypassResponse = handleBypassRequest(body, model, userAgent, ccFilterNaming); if (bypassResponse) return bypassResponse; const alias = PROVIDER_ID_TO_ALIAS[provider] || provider; diff --git a/open-sse/utils/bypassHandler.js b/open-sse/utils/bypassHandler.js index 1c57dc5..57fa2ff 100644 --- a/open-sse/utils/bypassHandler.js +++ b/open-sse/utils/bypassHandler.js @@ -8,7 +8,7 @@ import { formatSSE } from "./stream.js"; * Check for bypass patterns - return fake response without calling provider * Only works for Claude CLI requests */ -export function handleBypassRequest(body, model, userAgent = "") { +export function handleBypassRequest(body, model, userAgent = "", ccFilterNaming = false) { if (!userAgent.includes("claude-cli")) return null; if (!body.messages?.length) return null; @@ -22,6 +22,7 @@ export function handleBypassRequest(body, model, userAgent = "") { }; let shouldBypass = false; + let namingBypass = false; // Pattern 1: Title extraction (assistant message = "{") const lastMsg = messages[messages.length - 1]; @@ -54,23 +55,50 @@ export function handleBypassRequest(body, model, userAgent = "") { } } + // Pattern 5: CC naming request (topic title extraction by Claude Code CLI) + // Claude format: system is top-level body.system field, not inside messages + if (!shouldBypass && ccFilterNaming) { + const systemMsg = messages.find(m => m.role === "system"); + const systemFromMessages = getText(systemMsg?.content); + const systemFromBody = Array.isArray(body.system) + ? body.system.filter(s => s.type === "text").map(s => s.text).join(" ") + : (typeof body.system === "string" ? body.system : ""); + const systemText = systemFromMessages || systemFromBody; + if (systemText.includes("isNewTopic")) { + shouldBypass = true; + namingBypass = true; + } + } + if (!shouldBypass) return null; const sourceFormat = detectFormat(body); const stream = body.stream !== false; + // For naming bypass, generate title from user message + if (namingBypass) { + const userMsg = messages.find(m => m.role === "user"); + const userText = getText(userMsg?.content); + const title = userText.trim().split(/\s+/).slice(0, 3).join(" "); + const namingText = JSON.stringify({ isNewTopic: true, title }); + return stream + ? createStreamingResponse(sourceFormat, model, namingText) + : createNonStreamingResponse(sourceFormat, model, namingText); + } + return stream ? createStreamingResponse(sourceFormat, model) : createNonStreamingResponse(sourceFormat, model); } +const DEFAULT_BYPASS_TEXT = "CLI Command Execution: Clear Terminal"; + /** * Create OpenAI standard format response */ -function createOpenAIResponse(model) { +function createOpenAIResponse(model, text = DEFAULT_BYPASS_TEXT) { const id = `chatcmpl-${Date.now()}`; const created = Math.floor(Date.now() / 1000); - const text = "CLI Command Execution: Clear Terminal"; return { id, @@ -97,8 +125,8 @@ function createOpenAIResponse(model) { * Create non-streaming response with translation * Use translator to convert OpenAI → sourceFormat */ -function createNonStreamingResponse(sourceFormat, model) { - const openaiResponse = createOpenAIResponse(model); +function createNonStreamingResponse(sourceFormat, model, text) { + const openaiResponse = createOpenAIResponse(model, text); // If sourceFormat is OpenAI, return directly if (sourceFormat === FORMATS.OPENAI) { @@ -151,8 +179,8 @@ function createNonStreamingResponse(sourceFormat, model) { * Create streaming response with translation * Use translator to convert OpenAI chunks → sourceFormat */ -function createStreamingResponse(sourceFormat, model) { - const openaiResponse = createOpenAIResponse(model); +function createStreamingResponse(sourceFormat, model, text) { + const openaiResponse = createOpenAIResponse(model, text); const state = initState(sourceFormat); state.model = model; diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js index 4f5e4a0..77946bf 100644 --- a/open-sse/utils/stream.js +++ b/open-sse/utils/stream.js @@ -162,7 +162,9 @@ export function createSSEStream(options = {}) { const parsed = parseSSELine(trimmed, targetFormat); if (!parsed) continue; - if (parsed && parsed.done) { + // For Ollama: done=true is the final chunk with finish_reason/usage, must translate + // For other formats: done=true is the [DONE] sentinel, skip + if (parsed && parsed.done && targetFormat !== FORMATS.OLLAMA) { const output = "data: [DONE]\n\n"; reqLogger?.appendConvertedChunk?.(output); controller.enqueue(sharedEncoder.encode(output)); diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js index 2db68c9..e518b9b 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; -import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; +import { Card, Button, ModelSelectModal, ManualConfigModal, Tooltip } from "@/shared/components"; import Image from "next/image"; const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; @@ -31,6 +31,7 @@ export default function ClaudeToolCard({ const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); + const [ccFilterNaming, setCcFilterNaming] = useState(false); const hasInitializedModels = useRef(false); const getConfigStatus = () => { @@ -64,6 +65,22 @@ export default function ClaudeToolCard({ if (isExpanded) fetchModelAliases(); }, [isExpanded]); + useEffect(() => { + fetch("/api/settings").then(r => r.json()).then(data => { + setCcFilterNaming(!!data.ccFilterNaming); + }).catch(() => {}); + }, []); + + const handleCcFilterNamingToggle = async (e) => { + const value = e.target.checked; + setCcFilterNaming(value); + await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ccFilterNaming: value }), + }).catch(() => {}); + }; + const fetchModelAliases = async () => { try { const res = await fetch("/api/models/alias"); @@ -319,6 +336,19 @@ export default function ClaudeToolCard({ {modelMappings[model.alias] && } ))} + + {/* CC Filter Naming */} +
+ Filter naming + arrow_forward + + + info + +
{message && ( diff --git a/src/shared/components/Tooltip.js b/src/shared/components/Tooltip.js new file mode 100644 index 0000000..b7d0c0c --- /dev/null +++ b/src/shared/components/Tooltip.js @@ -0,0 +1,19 @@ +"use client"; + +export default function Tooltip({ text, children, position = "top" }) { + const posClass = { + top: "bottom-full left-1/2 -translate-x-1/2 mb-1.5", + bottom: "top-full left-1/2 -translate-x-1/2 mt-1.5", + left: "right-full top-1/2 -translate-y-1/2 mr-1.5", + right: "left-full top-1/2 -translate-y-1/2 ml-1.5", + }[position]; + + return ( +
+ {children} +
+ {text} +
+
+ ); +} diff --git a/src/shared/components/index.js b/src/shared/components/index.js index 56b3325..65f2bbc 100644 --- a/src/shared/components/index.js +++ b/src/shared/components/index.js @@ -25,6 +25,7 @@ export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal"; export { default as CursorAuthModal } from "./CursorAuthModal"; export { default as IFlowCookieModal } from "./IFlowCookieModal"; export { default as SegmentedControl } from "./SegmentedControl"; +export { default as Tooltip } from "./Tooltip"; // Layouts export * from "./layouts"; diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js index c711f3d..e7ed191 100644 --- a/src/sse/handlers/chat.js +++ b/src/sse/handlers/chat.js @@ -171,6 +171,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re } // Use shared chatCore + const chatSettings = await getSettings(); const result = await handleChatCore({ body: { ...body, model: `${provider}/${model}` }, modelInfo: { provider, model }, @@ -180,6 +181,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re connectionId: credentials.connectionId, userAgent, apiKey, + ccFilterNaming: !!chatSettings.ccFilterNaming, // Detect source format by endpoint + body sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null, onCredentialsRefreshed: async (newCreds) => {