fix(combo): keep nested combos as single units and stop 0-token detail rows
Nested combos (comboA lists comboB, comboC, …) now stay one slot each: the inner combo always runs as fallback to produce a single answer. Failed hops are no longer written to Details/usage, and streaming no longer inserts a 0-token placeholder row. - chat.js: comboStack cycle detection; nested combos forced to fallback; persistUsage="success-only" for combo hops - combo.js: discardResponse() cancels unused bodies (fusion timeout / fallback) so dropped streams fire onStreamComplete; getComboModelsFromData keeps nested names and honors enabled=false - requestDetail.js: tokensForDetail() canonicalizes Claude/Gemini usage; shouldPersistRequestDetail() skips streaming-start and non-success hops - streamingHandler.js: drop the 0-token streaming placeholder write - RequestDetailsTab.js: read Gemini/Claude token names; show "streaming" status in amber - tests: add combo-nested.test.js (13 cases) - gitignore: ignore local .vitest/ artifacts Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
||||
extractRequestConfig,
|
||||
saveUsageStats,
|
||||
formatDoneLine,
|
||||
tokensForDetail,
|
||||
shouldPersistRequestDetail,
|
||||
} from "./requestDetail.js";
|
||||
import { streamStatusForContent } from "../../utils/streamErrorPatterns.js";
|
||||
import { saveRequestDetail } from "@/lib/usageDb.js";
|
||||
@@ -60,7 +62,7 @@ function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent,
|
||||
/**
|
||||
* Handle streaming response — pipe provider SSE through transform stream to client.
|
||||
*/
|
||||
export async function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, customToolNames, streamController, onStreamComplete, streamDetailId, pxpipe, reqTag, log, credentials }) {
|
||||
export async function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, customToolNames, streamController, onStreamComplete, pxpipe, reqTag, log, credentials }) {
|
||||
if (onRequestSuccess) {
|
||||
Promise.resolve()
|
||||
.then(onRequestSuccess)
|
||||
@@ -145,35 +147,6 @@ export async function handleStreamingResponse({ providerResponse, provider, mode
|
||||
stallTimeoutMs,
|
||||
);
|
||||
|
||||
saveRequestDetail(
|
||||
buildRequestDetail(
|
||||
{
|
||||
provider,
|
||||
model,
|
||||
connectionId,
|
||||
apiKey,
|
||||
latency: { ttft: 0, total: Date.now() - requestStartTime },
|
||||
tokens: { prompt_tokens: 0, completion_tokens: 0 },
|
||||
request: extractRequestConfig(body, stream),
|
||||
providerRequest: finalBody || translatedBody || null,
|
||||
providerResponse: "[Streaming - raw response not captured]",
|
||||
response: {
|
||||
content: "[Streaming in progress...]",
|
||||
thinking: null,
|
||||
type: "streaming",
|
||||
},
|
||||
pxpipe,
|
||||
status: "success",
|
||||
},
|
||||
{ id: streamDetailId },
|
||||
),
|
||||
).catch((err) => {
|
||||
console.error(
|
||||
"[RequestDetail] Failed to save streaming request:",
|
||||
err.message,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
response: new Response(transformedBody, { headers: SSE_HEADERS }),
|
||||
@@ -198,6 +171,7 @@ export function buildOnStreamComplete({
|
||||
reqTag,
|
||||
log,
|
||||
streamErrorPatterns,
|
||||
persistUsage = "all",
|
||||
}) {
|
||||
const streamDetailId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
@@ -210,37 +184,39 @@ export function buildOnStreamComplete({
|
||||
const safeThinking = contentObj?.thinking || null;
|
||||
const rawProviderText = typeof contentObj?.rawProviderText === "string" ? contentObj.rawProviderText : "";
|
||||
|
||||
saveRequestDetail(
|
||||
buildRequestDetail(
|
||||
{
|
||||
provider,
|
||||
model,
|
||||
connectionId,
|
||||
apiKey,
|
||||
latency,
|
||||
tokens: usage || { prompt_tokens: 0, completion_tokens: 0 },
|
||||
request: extractRequestConfig(body, stream),
|
||||
providerRequest: finalBody || translatedBody || null,
|
||||
providerResponse: rawProviderText || safeContent,
|
||||
response: {
|
||||
content: safeContent,
|
||||
thinking: safeThinking,
|
||||
type: "streaming",
|
||||
if (shouldPersistRequestDetail(persistUsage, "success")) {
|
||||
saveRequestDetail(
|
||||
buildRequestDetail(
|
||||
{
|
||||
provider,
|
||||
model,
|
||||
connectionId,
|
||||
apiKey,
|
||||
latency,
|
||||
tokens: tokensForDetail(usage),
|
||||
request: extractRequestConfig(body, stream),
|
||||
providerRequest: finalBody || translatedBody || null,
|
||||
providerResponse: rawProviderText || safeContent,
|
||||
response: {
|
||||
content: safeContent,
|
||||
thinking: safeThinking,
|
||||
type: "streaming",
|
||||
},
|
||||
pxpipe,
|
||||
status: streamStatusForContent(
|
||||
streamErrorPatterns?.[provider],
|
||||
safeContent,
|
||||
),
|
||||
},
|
||||
pxpipe,
|
||||
status: streamStatusForContent(
|
||||
streamErrorPatterns?.[provider],
|
||||
safeContent,
|
||||
),
|
||||
},
|
||||
{ id: streamDetailId },
|
||||
),
|
||||
).catch((err) => {
|
||||
console.error(
|
||||
"[RequestDetail] Failed to update streaming content:",
|
||||
err.message,
|
||||
);
|
||||
});
|
||||
{ id: streamDetailId },
|
||||
),
|
||||
).catch((err) => {
|
||||
console.error(
|
||||
"[RequestDetail] Failed to update streaming content:",
|
||||
err.message,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Persist stream usage to DB (no console line; the "📊 done" line below is authoritative)
|
||||
saveUsageStats({
|
||||
|
||||
Reference in New Issue
Block a user